Co może skłonić tak dużą firmę jak Lamoda z ustabilizowanym procesem i dziesiątkami powiązanych usług do istotnej zmiany podejścia? Motywacja może być bardzo różna: od legislacyjnej po przyrodzoną wszystkim programistom chęć eksperymentowania.
Jednak to nie znaczy, że nie można liczyć na dodatkowe korzyści. O tym, co konkretnie można zyskać, wdrażając API oparte na zdarzeniach w Kafce, opowie Sergey Zaika (). O nabitych szczołach i interesujących odkryciach też na pewno będzie — nie może się obejść bez eksperymentów.

Disclaimer: Ten artykuł oparty jest na materiałach z meetup'a, który Sergey przeprowadził w listopadzie 2018 roku na HighLoad++. Żywe doświadczenia Lamody z Kafką przyciągnęły słuchaczy nie mniej niż inne wykłady w harmonogramie. Uważamy, że to doskonały przykład tego, że zawsze można i należy szukać podobnie myślących ludzi, a organizatorzy HighLoad++ będą dalej starać się tworzyć atmosferę do tego sprzyjającą.
O procesie
Lamoda to duża platforma e-commerce, która posiada własne centrum kontaktowe, usługę dostawy (i wiele partnerów), studio fotograficzne, ogromny magazyn i wszystko to działa na własnym oprogramowaniu. Istnieją dziesiątki metod płatności, partnerzy b2b, którzy mogą korzystać z części lub wszystkich tych usług i chcą znać aktualne informacje o swoich produktach. Dodatkowo, Lamoda działa w trzech krajach oprócz Rosji i wszędzie jest trochę inaczej. Łącznie może być ponad sto sposobów skonfigurowania nowego zamówienia, które musi być przetwarzane na swój sposób. Wszystko to działa dzięki dziesiątkom usług, które komunikują się czasami w sposób nieoczywisty. Jest też centralny system, którego główną odpowiedzialnością są statusy zamówień. Nazywamy go BOB, ja pracuję z tym systemem.
Refund Tool with events-driven API
Termin events-driven jest dość wyświechtany, trochę później dokładniej określimy, co przez to rozumiemy. Zacznę od kontekstu, w którym postanowiliśmy wypróbować podejście API oparte na zdarzeniach w Kafce.

W każdym sklepie, oprócz zamówień, za które klienci płacą, są momenty, kiedy od sklepu wymaga się zwrotu pieniędzy, ponieważ towar nie odpowiadał klientowi. Ten stosunkowo krótki proces: w razie potrzeby ustalamy informacje i przekazujemy pieniądze.
Jednak zwrot stał się bardziej skomplikowany z powodu zmian w przepisach, więc musieliśmy wdrożyć oddzielny mikroserwis do jego obsługi.

Nasza motywacja:
- Ustawa FZ-54 — w skrócie, ustawa wymaga zgłaszania do urzędów skarbowych każdej transakcji pieniężnej, niezależnie czy to zwrot, czy wpłata, w dość krótkim czasie SLA wynoszącym kilka minut. My, jako e-commerce, przeprowadzamy sporo operacji. Technicznie oznacza to nową odpowiedzialność (a więc nowy serwis) oraz modyfikacje w wszystkich zaangażowanych systemach.
- BOB split — wewnętrzny projekt firmy mający na celu uwolnienie BOB od dużej liczby zbędnych odpowiedzialności i zmniejszenie jego złożoności.

Na tym schemacie przedstawione są główne systemy Lamoda. Obecnie większość z nich przypomina raczej konstelację 5-10 mikroserwisów wokół kurczącego się monolitu.Rosną one powoli, ale staramy się je zmniejszać, ponieważ wdrażanie wyodrębnionego fragmentu w środku budzi obawy – nie można dopuścić do jego awarii. Wszystkie połączenia (strzałki) musimy rezerwować, zakładając, że każdy z nich może być niedostępny.
W BOB również jest sporo połączeń: systemy płatności, dostawy, powiadomień itd.
Technicznie BOB to:
- ~150k linijek kodu + ~100k linijek testów;
- php7.2 + Zend 1 & Symfony Components 3;
- >100 API & ~50 integracji zewnętrznych;
- 4 kraje z własną logiką biznesową.
Wdrażanie BOB jest kosztowne i bolesne, ilość kodu i rozwiązywanych przez niego zadań jest taka, że nikt nie może go w całości zrozumieć. Ogólnie rzecz biorąc, jest wiele powodów, aby go uprościć.
Proces zwrotu
Początkowo w proces zaangażowane są dwa systemy: BOB i Payment. Teraz dołączają jeszcze dwa:
- Usługa Fiskalizacji, która zajmie się problemami z fiskalizacją oraz komunikacją z zewnętrznymi serwisami.
- Narzędzie Zwrotów, do którego po prostu przenoszone są nowe połączenia, aby nie rozbudowywać BOB.
Teraz proces wygląda tak:

- Do BOB przychodzi żądanie zwrotu pieniędzy.
- BOB informuje o tym Narzędzie Zwrotów.
- Narzędzie Zwrotów mówi Payment: „Zwróć pieniądze”.
- Payment zwraca pieniądze.
- Narzędzie Zwrotów i BOB synchronizują się nawzajem z statusami, ponieważ obecnie obie strony tego potrzebują. Na razie nie jesteśmy gotowi na całkowite przełączenie się na Narzędzie Zwrotów, ponieważ w BOB są UI, raporty dla księgowości oraz wiele danych, które tak łatwo nie przeniesiesz. Musimy pozostać na dwóch krzesłach.
- Wysyłane jest żądanie fiskalizacji.
W rezultacie stworzyliśmy na Kafka pewnego rodzaju szynę zdarzeń – event-bus, na której wszystko się opiera. Hurra, teraz mamy jeden punkt awarii (sarkazm).

Plusy i minusy są dość oczywiste. Zrobiliśmy szynę, co oznacza, że teraz wszystkie serwisy od niej zależą. To upraszcza projektowanie, ale wprowadza w systemie jeden punkt awarii. Jeśli Kafka padnie, proces stanie.
Czym jest API oparte na zdarzeniach
Dobrą odpowiedź na to pytanie można znaleźć w wykładzie Martina Fowlera (GOTO 2017) .
Krótko mówiąc, co zrobiliśmy:
- Obudowaliśmy wszystkie asynchroniczne wymiany przez przechowywanie zdarzeń. Zamiast informować przez sieć każdego zainteresowanego konsumenta o zmianie statusu, zapisujemy w zcentralizowanym magazynie zdarzenie o zmianie stanu, a zainteresowani w temacie konsumenci odczytują wszystko, co się pojawia.
- Zdarzenie (event) w tym przypadku to powiadomienie (notifications) o tym, że coś gdzieś się zmieniło. Na przykład zmienił się status zamówienia. Konsument, któremu ważne są pewne dodatkowe dane związane z zmianą statusu, a których nie ma w powiadomieniu, może sam poznać ich stan.
- Maksymalna wersja to pełne źródło zdarzeń, przesyłanie stanu, w którym zdarzenie zawiera wszystkie informacje potrzebne do przetworzenia: skąd i w jaki status przeszli, jak dokładnie zmieniły się dane itp. Pytanie tylko o celowość i objętość informacji, którą możesz sobie pozwolić przechowywać.
W ramach uruchomienia Refund Tool użyliśmy trzeciej opcji. Ułatwiło to przetwarzanie zdarzeń, ponieważ nie trzeba pozyskiwać szczegółowych informacji, a ponadto wykluczyło scenariusz, w którym każde nowe zdarzenie wywołuje lawinę uzupełniających zapytań GET od konsumentów.
Usługa Refund Tool nie jest obciążona, dlatego Kafka jest tam raczej próbą swoich sił, niż koniecznością. Nie sądzę, że gdyby serwis zwrotu środków stał się projektem o dużym obciążeniu, biznes byłby z tego powodu zadowolony.
Asynchroniczna wymiana AS IS
Dla asynchronicznych wymian, dział PHP zazwyczaj używa RabbitMQ. Zbieramy dane do zapytania, wkładamy je do kolejki, a konsument tej samej usługi je odczytuje i wysyła (lub nie wysyła). Dla samego API Lamoda aktywnie wykorzystuje Swagger. Projektujemy API, opisujemy je w Swaggerze, generujemy kod kliencki i serwerowy. Używamy również nieco rozszerzonego JSON RPC 2.0.
Gdzieś używane są szyny esb, ktoś korzysta z activeMQ, ale ogólnie, RabbitMQ — standard.
Async exchange DO
Projektując wymianę przez events-bus, zauważamy analogię. W podobny sposób opisujemy przyszłą wymianę danych poprzez opisy struktury eventu. Format yaml, kodogenaracja musieliśmy zrobić sami, generator według specyfikacji tworzy DTO i uczy klientów oraz serwery ich używać. Generacja odbywa się w dwóch językach — golang i php. To pozwala utrzymywać biblioteki w zgodności. Generator jest napisany w golang, stąd jego nazwa gogi.
Event-sourcing w Kafka — rzecz typowa. Jest rozwiązanie od głównej wersji enterprise Kafka Confluent, jest , rozwiązanie od naszych „braci” z dziedziny Zalando. Nasza motywacja, by zacząć od vanilla Kafka — to pozostawienie rozwiązania darmowym, aż do momentu, gdy zdecydujemy, czy będziemy je powszechnie stosować, a także aby mieć przestrzeń do manewru i poprawek: chcemy wsparcia dla naszego JSON RPC 2.0, generatorów pod dwa języki i zobaczymy, co jeszcze.
Ironia polega na tym, że nawet w takim szczęśliwym przypadku, kiedy istnieje mniej więcej podobny biznes jak Zalando, który stworzył podobne rozwiązanie, nie możemy go efektywnie wykorzystać.
Architektonicznie na początku mamy taki wzorzec: czytamy bezpośrednio z Kafka, ale zapisujemy tylko przez events-bus. Do czytania z Kafka jest wiele gotowego: brokerzy, load balancery, i jest w miarę gotowe do poziomego skalowania, co chcieliśmy zachować. Zapis z kolei zdecydowaliśmy się zawinąć przez jeden Gateway aka Events-bus, i oto dlaczego.
Events-bus
Czyli autobus wydarzeń. To po prostu stateless http gateway, który przejmuje na siebie kilka ważnych ról:
- Walidacja produkcji — sprawdzamy, czy wydarzenia odpowiadają naszej specyfikacji.
- System nadrzędny do wydarzeń, czyli to główny i jedyny system w firmie, który odpowiada na pytanie, które wydarzenia z jakimi strukturami są uznawane za ważne. W walidacji zawarte są po prostu typy danych i enumy dla ścisłej specyfikacji zawartości.
- Funkcja haszująca do sharding — struktura wiadomości Kafka to key-value i według hasha z key oblicza się, gdzie to umieścić.
Dlaczego
Pracujemy w dużej firmie z ustalonym procesem. Po co coś zmieniać? To eksperyment, i spodziewamy się uzyskać kilka korzyści.
1:n+1 wymiany (jeden do wielu)
Przy Kafka bardzo łatwo podłączyć nowych konsumentów do API.
Załóżmy, że masz poradnik, który trzeba utrzymywać aktualnym w kilku systemach naraz (i w jakichś nowych). Kiedyś wymyśliliśmy pakiet, który realizował set-API, a główny system informował o adresach konsumentów. Teraz główny system wysyła aktualizacje do tematu, a wszyscy, którzy są zainteresowani, je czytają. Pojawił się nowy system — podłączony do tematu. Tak, również pakiet, ale prostszy.
W przypadku narzędzia zwrotów, które jest częścią BOB, wygodnie jest nam zachować je synchronizowane przez Kafka. Payment informuje, że pieniądze zostały zwrócone: BOB, RT dowiadują się o tym, zmieniają swoje statusy, a Fiscalization Service także o tym wie i wystawia paragon.

Mamy plany stworzenia jednego Serwisu Powiadomień, który miałby informować klienta o nowościach związanych z jego zamówieniem/zwrotami. Teraz ta odpowiedzialność jest rozproszona pomiędzy systemy. Wystarczy, że nauczymy Serwis Powiadomień wyłapywać z Kafka odpowiednie informacje i na nie reagować (i wyłączyć te powiadomienia w innych systemach). Nie będą potrzebne żadne nowe bezpośrednie wymiany.
Zarządzane danymi
Informacje między systemami stają się przejrzyste — niezależnie od tego, jaki „krwawy enterprise” masz, i jak obszerny jest twój backlog. W Lamoda jest dział analizy danych, który zbiera dane z systemów i przekształca je w formę do ponownego wykorzystania, zarówno dla biznesu, jak i dla systemów inteligentnych. Kafka pozwala szybko dostarczyć im wiele danych i utrzymywać ten przepływ informacji aktualnym.
Dziennik replikacji
Wiadomości nie znikają po przeczytaniu, jak w RabbitMQ. Kiedy zdarzenie zawiera wystarczająco dużo informacji do przetwarzania, mamy historię ostatnich zmian obiektu, a jeśli chcemy, możliwość zastosowania tych zmian.
Czas przechowywania dziennika replikacji zależy od intensywności zapisu w tym temacie, Kafka pozwala elastycznie ustawić limity zarówno pod względem czasu przechowywania, jak i objętości danych. W przypadku intensywnych tematów ważne jest, aby wszyscy konsumenci zdążyli przeczytać informacje przed ich zniknięciem, nawet w przypadku krótkotrwałej awarii. Zazwyczaj udaje się przechowywać dane przez jednostki dni, co jest wystarczające dla wsparcia.

Teraz trochę przeglądu dokumentacji, dla tych, którzy nie są zaznajomieni z Kafka (rysunek też z dokumentacji)
W AMQP istnieją kolejki: zapisujemy wiadomości do kolejki dla konsumenta. Zazwyczaj jedna kolejka jest obsługiwana przez jeden system z tą samą logiką biznesową. Jeśli trzeba powiadomić kilka systemów, można nauczyć aplikację zapisywać w kilku kolejkach lub skonfigurować exchange z mechanizmem fanout, który je klonuje.
W Kafka istnieje podobna abstrakcja temat, do którego zapisujesz wiadomości, ale one nie znikają po odczycie. Domyślnie, przy połączeniu z Kafka, otrzymujesz wszystkie wiadomości, a jednocześnie istnieje możliwość zapisania miejsca, w którym się zatrzymałeś. To znaczy, że czytasz sekwencyjnie, możesz nie oznaczać wiadomości jako przeczytanej, ale zapisać id, od którego później wznowisz czytanie. Id, na którym się zatrzymałeś, nazywa się offset (przesunięcie), a mechanizm to commit offset.
Odpowiednio, można zrealizować różną logikę. Na przykład, nasz BOB istnieje w 4 instancjach dla różnych krajów – Lamoda jest w Rosji, Kazachstanie, Ukrainie i Białorusi. Ponieważ są one wdrażane oddzielnie, mają nieco własne konfiguracje i swoją logikę biznesową. Wskazujemy w wiadomości, do którego kraju się odnosi. Każdy konsument BOB w każdym kraju czyta z różnymi groupId, a jeśli wiadomość do niego nie pasuje, pomijają ją, tj. od razu commitują offset +1. Jeśli ten sam temat czyta nasza usługa płatności, robi to z oddzielną grupą, więc offsety się nie krzyżują.
Wymagania dotyczące zdarzeń:
- Pełność danych. Chcielibyśmy, aby w zdarzeniu znajdowało się wystarczająco dużo danych, aby można je było przetworzyć.
- Integralność. Delegujemy Events-bus sprawdzenie, że zdarzenie jest spójne i może je przetworzyć.
- Kolejność ma znaczenie. W przypadku zwrotu musimy pracować z historią. W przypadku powiadomień kolejność nie ma znaczenia, jeśli są to jednorodne powiadomienia, e-mail będzie taki sam, niezależnie od tego, który zamówienie przybyło jako pierwszy. W przypadku zwrotu istnieje wyraźny proces, jeśli zmienisz kolejność, mogą wystąpić wyjątki, zwrot nie zostanie utworzony lub nie zostanie przetworzony – przejdziemy do innego statusu.
- Spójność. Mamy magazyn, a teraz zamiast API tworzymy zdarzenia. Potrzebujemy sposobu na szybkie i tanie przekazywanie naszym usługom informacji o nowych zdarzeniach i zmianach w już istniejących. Osiągamy to dzięki wspólnej specyfikacji w osobnym repozytorium git oraz generatorom kodu. Dlatego klienci i serwery w różnych usługach są u nas zsynchronizowane.
Kafka w Lamoda
Mamy trzy instalacje Kafka:
- Logi;
- R&D;
- Events-bus.
Dziś mówimy tylko o ostatnim punkcie. W events-bus mamy niezbyt duże instalacje - 3 brokerów (serwery) i tylko 27 tematów. Zazwyczaj jeden temat to jeden proces. Ale to delikatna kwestia, którą za chwilę poruszymy.

Powyżej wykres rps. Proces zwrotów oznaczony jest turkusową linią (tak, tak, tą na osi X), a różową - proces aktualizacji treści.
Katalog Lamoda zawiera miliony produktów, a dane są ciągle aktualizowane. Niektóre kolekcje wychodzą z mody, w ich miejsce wprowadzane są nowe, w katalogu ciągle pojawiają się nowe modele. Staramy się przewidzieć, co będzie interesować naszych klientów jutro, dlatego ciągle kupujemy nowe rzeczy, robimy im zdjęcia i aktualizujemy witrynę.
Różowe szczyty to aktualizacja produktów, czyli zmiany dotyczące towarów. Widać, że chłopaki fotografowały, fotografowały, a potem nagle! — załadowali paczkę zdarzeń.
Przykłady użycia Lamoda Events
Zbudowaną architekturę wykorzystujemy do następujących operacji:
- Śledzenie statusów zwrotów: call-to-action i śledzenie statusów ze wszystkich zaangażowanych systemów. Płatność, statusy, fiskalizacja, powiadomienia. Tutaj wypróbowaliśmy podejście, stworzyliśmy narzędzia, zebraliśmy wszystkie błędy, napisaliśmy dokumentację i opowiedzieliśmy kolegom, jak z tego korzystać.
- Aktualizacja kart produktu: konfiguracja, metadane, specyfikacje. Czyta jeden system (który wyświetla), a pisze ich kilka.
- Email, push i sms: zamówienie zostało zrealizowane, zamówienie dotarło, zwrot został przyjęty itd., jest ich wiele.
- Stan, aktualizacja zapasów — ilościowa aktualizacja nazw, po prostu liczby: dostawa do magazynu, zwrot. Musi być tak, aby wszystkie systemy związane z rezerwowaniem towaru operowały maksymalnie aktualnymi danymi. Obecnie system aktualizacji zapasów jest dość skomplikowany, Kafka pozwoli go uprościć.
- Analiza danych (Dział R&D), narzędzia ML, analityka, statystyka. Chcemy, żeby informacje były przejrzyste - do tego Kafka jest dobrze dopasowana.
Teraz bardziej interesująca część dotycząca zbierania doświadczeń i interesujących odkryć, które miały miejsce w ciągu pół roku.
Problemy projektowe
Załóżmy, że chcemy stworzyć nową rzecz - na przykład, przenieść cały proces dostawy na Kafka. Obecnie część procesu jest realizowana w Order Processing w BOB. Po przekazaniu zamówienia do usługi dostawy, jego przemieszczeniu na magazyn pośredni i innych rzeczach istnieje model statusów. Jest cały monolit, nawet dwa, a do tego mnóstwo API poświęconych dostawie. Wiedzą o dostawie znacznie więcej.
Wydaje się, że to podobne obszary, ale dla Order Processing w BOB i dla systemu dostawy statusy różnią się. Na przykład, niektóre firmy kurierskie nie wysyłają statusów pośrednich, a tylko końcowe: „dostarczono” lub „zgubiono”. Inne, wręcz przeciwnie, bardzo szczegółowo informują o przemieszczeniu towaru. Każdy ma swoje zasady walidacji: dla niektórych, jeśli email jest ważny, to zostanie przetworzony; dla innych - nie ważny, ale zamówienie i tak będzie przetworzone, ponieważ jest telefon do kontaktu, a niektórzy powiedzą, że takie zamówienie w ogóle nie będzie przetwarzane.
Strumień danych
W przypadku Kafki pojawia się pytanie o organizację strumienia danych. To zadanie wiąże się z wyborem strategii w kilku punktach, przejdźmy przez wszystkie z nich.
Do jednego topiku czy do różnych?
Mamy specyfikację zdarzenia. W BOB piszemy, że takie zamówienie trzeba dostarczyć i wskazujemy: numer zamówienia, jego skład, jakieś SKU i kody kreskowe itd. Gdy towar przybędzie do magazynu, dostawa będzie mogła otrzymać statusy, znaczniki czasu i wszystko co potrzebne. Ale później chcemy w BOB otrzymywać aktualizacje na temat tych danych. Pojawia się więc proces odwrotnego pozyskiwania danych z dostawy. Czy to to samo zdarzenie? Czy jest to osobna wymiana, która zasługuje na osobny topik?
Prawdopodobnie będą one bardzo podobne, a pokusa stworzenia jednego topiku nie jest bezpodstawna, ponieważ osobny topik to osobni konsumenci, osobne konfiguracje, osobne generowanie tego wszystkiego. Ale nie ma pewności.
Nowe pole czy nowe zdarzenie?
Jednak jeśli użyjemy tych samych zdarzeń, pojawia się inny problem. Na przykład nie wszystkie systemy dostawy mogą wygenerować taki DTO, który potrafiłby generować BOB. Wysyłamy im id, a oni ich nie zapisują, ponieważ ich nie potrzebują, a z punktu widzenia rozpoczęcia procesu event-bus to pole jest obowiązkowe.
Jeśli wprowadzimy dla event-bus zasadę, że to pole jest obowiązkowe, będziemy zmuszeni w BOB lub w obsłudze zdarzenia startowego ustawić dodatkowe zasady walidacji. Walidacja zaczyna się rozprzestrzeniać po serwisie - to nie jest zbyt wygodne.
Kolejnym problemem jest pokusa inkrementalnego rozwoju. Mówią nam, że trzeba dodać coś do zdarzenia, a być może, jeśli dobrze pomyśleć, powinno to być oddzielne zdarzenie. Ale w naszym schemacie oddzielne zdarzenie to oddzielny temat. Oddzielny temat to cały ten proces, który opisałem powyżej. Programista ma pokusę, aby po prostu dodać jeszcze jedno pole do schemy JSON i wygenerować ją ponownie.
W przypadku refundacji przez pół roku dotarliśmy do zdarzenia zdarzeń. Mieliśmy jedno meta-zdarzenie, które nazywa się refund update, w którym było pole type, opisujące, na czym właściwie polega ta aktualizacja. Od tego mieliśmy "wspaniałe" przełączniki z walidatorami, które mówiły, jak należy walidować to zdarzenie z tym typem.
Wersjonowanie zdarzeń
Do walidacji wiadomości w Kafka można używać , ale trzeba to od razu uwzględnić i użyć Confluent. W naszym przypadku z wersjonowaniem musimy być ostrożni. Nie zawsze będzie możliwe przeczytanie wiadomości z logu replikacji, ponieważ model „odjechał”. W zasadzie trzeba budować wersje tak, aby model był wstecznie kompatybilny: na przykład uczynić pole tymczasowo nieobowiązkowym. Jeśli różnice są zbyt duże, zaczynamy pisać w nowy temat, a klientów przesiadujemy, gdy przeczytają stary.
Gwarancja kolejności odczytu partycji
Tematy wewnątrz Kafka są podzielone na partycje. Nie jest to zbyt ważne, gdy projektujemy byty i wymiany, ale ważne, gdy decydujemy, jak to konsumować i skalować.
W normalnym przypadku wysyłasz do Kafka jeden temat. Domyślnie używany jest jeden partycja, a wszystkie wiadomości tego tematu trafiają do niej. Konsument odpowiednio kolejno odczytuje te wiadomości. Załóżmy, że teraz musisz rozszerzyć system tak, aby wiadomości czytały dwa różne konsumenty. Jeśli na przykład wysyłasz SMS, możesz powiedzieć Kafka, aby utworzyła dodatkową partycję, a Kafka zacznie rozdzielać wiadomości na dwie części - połowę tam, połowę tutaj.
Jak Kafka je dzieli? Każda wiadomość ma ciało (w którym przechowujemy JSON) oraz klucz. Do tego klucza można zastosować funkcję haszującą, która będzie określać, do której partycji trafi wiadomość.
W naszym przypadku z refundacjami jest to ważne, jeśli bierzemy dwie partycje, to istnieje szansa, że równoległy konsument przetworzy drugie zdarzenie wcześniej niż pierwsze, co może być problematyczne. Funkcja haszująca gwarantuje, że wiadomości z tym samym kluczem trafią do tej samej partycji.
Zdarzenia vs polecenia
To jeszcze jeden problem, z którym się spotkaliśmy. Zdarzenie to pewne wydarzenie: mówimy, że coś gdzieś się wydarzyło (something_happened), na przykład, przedmiot został anulowany lub wystąpił zwrot. Jeśli ktoś słucha tych zdarzeń, to po "przedmiot został anulowany" zostanie utworzona encja zwrotu, a "wystąpił zwrot" zostanie zapisane gdzieś w ustawieniach.
Jednak zazwyczaj, gdy projektujesz zdarzenia, nie chcesz ich pisać na darmo - zakładasz, że ktoś je przeczyta. Jest duża pokusa, aby napisać coś innego niż something_happened (item_canceled, refund_refunded), a raczej something_should_be_done. Na przykład, przedmiot jest gotowy do zwrotu.
Z jednej strony to sugeruje, jak zdarzenie zostanie wykorzystane. Z drugiej strony, to znacznie mniej przypomina normalną nazwę zdarzenia. Poza tym, to już blisko do komendy do_something. Jednak nie masz gwarancji, że to zdarzenie ktoś przeczytał; a jeśli przeczytał, to przeczytał je poprawnie; a jeśli przeczytał poprawnie, to coś zrobił, i to coś przeszło pomyślnie. W momencie, gdy zdarzenie staje się do_something, potrzeba zwrotnej informacji staje się konieczna, i to jest problem.

W asynchronicznej wymianie w RabbitMQ, gdy przeczytasz wiadomość, pójdziesz na http, masz reakcję - przynajmniej, że wiadomość została przyjęta. Kiedy zapisałeś w Kafka, jest wiadomość, że zapisałeś w Kafka, ale nie wiesz, jak została przetworzona.
W związku z tym w naszym przypadku musieliśmy wprowadzić odpowiednie zdarzenie i skonfigurować monitoring, aby sprawdzić, czy po wystąpieniu określonej liczby zdarzeń w określonym czasie powinno nadejść tyle samo odpowiedzi. Jeśli to nie nastąpi, to wydaje się, że coś poszło nie tak. Na przykład, jeśli wysyłamy zdarzenie „item_ready_to_refund”, spodziewamy się, że zwrot zostanie zrealizowany, klient odzyska pieniądze, a my otrzymamy zdarzenie „money_refunded”. Ale to nie jest pewne, dlatego potrzebny jest monitoring.
Niemożności
Jest jeden dość oczywisty problem: jeśli odczytujesz z tematu sekwencyjnie, a masz jakieś złe wiadomości, konsument pada i dalej nie pójdziesz. Musisz zatrzymać wszystkich konsumentów, zatwierdzić offset dalej, aby móc kontynuować odczytywanie.
Wiedzieliśmy o tym, przewidzieliśmy to, a mimo to to się wydarzyło. A wydarzyło się to, ponieważ zdarzenie było poprawne z punktu widzenia events-bus, zdarzenie było poprawne z punktu widzenia walidatora aplikacji, ale nie było poprawne z punktu widzenia PostgreSQL, ponieważ w jednym systemie mieliśmy MySQL z UNSIGNED INT, a w nowym systemie był PostgreSQL z INT. Jego rozmiar jest nieco mniejszy i Id nie zmieścił się. Symfony zakończyło działanie z wyjątkiem. Oczywiście złapaliśmy wyjątek, ponieważ się na niego przygotowaliśmy i zamierzaliśmy zatwierdzić ten offset, ale wcześniej chcieliśmy inkrementować licznik problemów, gdyż wiadomość została nieprawidłowo przetworzona. Liczniki w tym projekcie także są przechowywane w bazie, a Symfony już zakończyło komunikację z bazą, a drugi wyjątek zabił cały proces bez szans na zatwierdzenie offsetu.
Przez jakiś czas serwis przestał działać — na szczęście z Kafka nie jest to takie straszne, ponieważ wiadomości pozostają. Kiedy praca zostanie wznowiona, będzie można je dokończyć. To jest wygodne.
Kafka ma możliwość ustawienia dowolnego offsetu przez tooling. Ale aby to zrobić, trzeba zatrzymać wszystkich konsumentów — w naszym przypadku przygotować oddzielne wydanie, w którym nie będzie konsumentów ani redeployments. Wtedy przez narzędzia Kafka można przesunąć offset i wiadomość przejdzie.
Inny niuans — log replikacji vs rdkafka.so — związane z charakterystyką naszego projektu. U nas PHP, a w PHP, jak zwykle, wszystkie biblioteki komunikują się z Kafka przez repozytorium rdkafka.so, a potem następuje jakaś nakładka. Może to nasze osobiste trudności, ale okazało się, że ponowne przeczytanie fragmentu już przeczytanego nie jest takie proste. Ogólnie mieliśmy problemy programowe.
Wracając do specyfiki pracy z partitions, w dokumentacji jest napisane consumers >= topic partitions. Ale dowiedziałem się o tym znacznie później, niż bym chciał. Jeśli chcesz się skalować i mieć dwóch konsumentów, potrzebujesz co najmniej dwóch partitions. To znaczy, jeśli miałeś jedną partycję, w której zgromadziło się 20 tysięcy wiadomości, a stworzyłeś nową, liczba wiadomości nie wyrówna się szybko. Dlatego, aby mieć dwóch równoległych konsumentów, musisz zrozumieć działanie partitions.
Monitoring
Myślę, że na podstawie tego, jak monitorujemy, będzie jeszcze jaśniej, jakie problemy występują w istniejącym podejściu.
Na przykład, liczymy, ile produktów w bazie niedawno zmieniło status, i odpowiednio, na podstawie tych zmian, powinny zdarzyć się wydarzenia, i wysyłamy tę liczbę do naszego systemu monitorowania. Następnie z Kafka otrzymujemy drugą liczbę, ile wiadomości faktycznie zostało zapisanych. Oczywiście, różnica między tymi dwiema liczbami zawsze powinna wynosić zero.

Ponadto potrzeba monitorować, jak radzi sobie producent, czy events-bus odebrał wiadomości i jak radzi sobie konsument. Na przykład, na poniższych wykresach u Refund Tool wszystko jest w porządku, a u BOB wyraźnie występują jakieś problemy (niebieskie szczyty).

Już wspominałem o consumer-group lag. Mówiąc prosto, to liczba nieprzeczytanych wiadomości. Ogólnie nasi konsumenci działają szybko, więc lag zazwyczaj wynosi 0, ale czasami może wystąpić krótkotrwały szczyt. Kafka potrafi to rozwiązać z pudełka, ale musisz ustawić jakiś interwał.
Jest projekt , który dostarczy Ci więcej informacji na temat Kafka. Po prostu przez API dla consumer-group zwraca status, jak radzi sobie ta grupa. Oprócz OQ i Failed jest też warning, dzięki czemu możesz dowiedzieć się, że Twoi konsumenci mają trudności z tempem produkcji — nie nadążają z odczytywaniem tego, co jest zapisywane. System jest dość inteligentny, łatwo go używać.

Tak wygląda odpowiedź przez API. Tutaj grupa bob-live-fifa, partycja refund.update.v1, status OK, lag 0 — ostatni końcowy offset to ten.

Monitoring updated_at SLA (stuck) Już wspominałem. Na przykład, towar przeszedł w status, że jest gotowy do zwrotu. Ustawiamy Cron, który mówi, że jeśli w ciągu 5 minut ten obiekt nie przeszedł w refund (zwracamy pieniądze przez systemy płatności bardzo szybko), to na pewno coś poszło nie tak, i to z pewnością jest przypadek dla supportu. Dlatego po prostu bierzemy Cron, który czyta takie rzeczy, i jeśli ich liczba jest większa od 0, to wysyła alert.
Podsumowując, korzystanie z zdarzeń jest wygodne, gdy:
- informacja jest potrzebna kilku systemom;
- wynik przetwarzania nie ma znaczenia;
- jest mało zdarzeń lub zdarzenia są małe.
Zdawałoby się, że temat artykułu jest dość konkretny — asynchroniczne API na Kafka, ale w związku z nim chcę od razu wiele rzeczy polecić.
Po pierwsze, następny nie trzeba czekać do listopada, już w kwietniu będzie jego wersja w Petersburgu, a w czerwcu porozmawiamy o dużych obciążeniach w Nowosybirsku.
Po drugie, autor referatu, Sergey Zaika, jest członkiem Komitetu Programowego naszej nowej konferencji poświęconej zarządzaniu wiedzą . Konferencja jest jednodniowa, odbędzie się 26 kwietnia, ale program jest bardzo bogaty.
A w maju odbędzie się i (z DevOpsConf w składzie) — tam jeszcze można zaproponować swój temat, opowiedzieć o swoim doświadczeniu i poskarżyć się na swoje zranione ego.
Źródło: habr.com
