Od 2019 roku w Rosji obowiązuje ustawa o obowiązkowym oznakowaniu. Ustawa nie dotyczy wszystkich grup towarów, a terminy wejścia w życie obowiązkowego oznakowania różnią się w zależności od grupy towarowej. Pierwszymi towarami objętymi obowiązkowym oznakowaniem są tytoń, obuwie, leki, a w późniejszym czasie dołączą inne produkty, takie jak perfumy, tekstylia, mleko. To ustawodawcze nowatorstwo spowodowało opracowanie nowych rozwiązań IT, które umożliwią śledzenie całego cyklu życia towaru od momentu produkcji do zakupu przez końcowego konsumenta, dla wszystkich uczestników procesu: zarówno dla państwa, jak i dla wszystkich organizacji sprzedających towary z obowiązkowym oznakowaniem.
W X5 system, który będzie śledził towary z oznakowaniem i wymieniał dane z państwem i dostawcami, otrzymał nazwę „Markus”. Opowiemy po kolei, jak i kto go opracowywał, jaki ma stos technologiczny i dlaczego mamy powody do dumy.

Prawdziwy HighLoad
„Markus” rozwiązuje wiele problemów, a najważniejszym z nich jest integracyjne połączenie między systemami informacyjnymi X5 a państwowym systemem informacyjnym oznakowanych produktów (GIS MP) w celu śledzenia ruchu oznakowanych produktów. Platforma przechowuje wszystkie kody oznakowania, które do nas wpłynęły, oraz całą historię ruchu tych kodów między obiektami, pomaga wyeliminować pomyłki w oznakowaniu produktów. Na przykładzie produktów tytoniowych, które weszły w pierwsze grupy oznakowanych towarów, tylko jedna ciężarówka papierosów zawiera około 600 000 paczek, z których każda ma unikalny kod. Naszym zadaniem jest śledzenie i weryfikacja legalności ruchów każdej takiej paczki między magazynami a sklepami, a na końcu sprawdzenie możliwości ich sprzedaży końcowemu klientowi. Rejestrujemy około 125 000 transakcji kasowych na godzinę, i musimy jeszcze zarejestrować, jak każda taka paczka trafiła do sklepu. W związku z tym, biorąc pod uwagę wszystkie ruchy między obiektami, spodziewamy się dziesiątek miliardów zapisów rocznie.
Zespół M
Mimo że projekt "Markus" w ramach X5 jest realizowany w oparciu o podejście produktowe, zespół pracuje w metodologii Scrum. Projekt rozpoczął się latem ubiegłego roku, ale pierwsze rezultaty przyszły dopiero w październiku — zespół został w pełni skompletowany, opracowano architekturę systemu i zakupiono sprzęt. Obecnie w zespole jest 16 osób, z czego sześć zajmuje się rozwojem backendu i frontendu, a trzy analizy systemowej. Ręcznym, obciążeniowym, automatycznym testowaniem oraz wsparciem produktu zajmuje się jeszcze sześć osób. Ponadto mamy specjalistę SRE.
Kod w naszym zespole piszą nie tylko programiści, prawie wszyscy potrafią programować i piszą testy automatyczne, skrypty obciążeniowe oraz skrypty automatyzacji. Poświęcamy temu szczególną uwagę, ponieważ nawet wsparcie produktu wymaga wysokiego poziomu automatyzacji. Koledzy, którzy wcześniej nie programowali, zawsze staramy się wspierać i pomagać, zlecać im jakieś drobne zadania.
W związku z pandemią koronawirusa przenieśliśmy cały zespół na pracę zdalną; posiadanie wszystkich narzędzi do zarządzania rozwojem oraz zbudowany workflow w Jira i GitLab pozwoliły na łatwe przejście przez ten etap. Miesiące spędzone na pracy zdalnej pokazały, że wydajność zespołu na tym nie ucierpiała, dla wielu komfort pracy się zwiększył, jedynie brakuje bezpośrednich kontaktów.
Spotkanie zespołu przed pracą zdalną

Spotkania podczas pracy zdalnej

Stos technologiczny rozwiązania
Standardowym repozytorium i narzędziem CI/CD dla X5 jest GitLab. Używamy go do przechowywania kodu, ciągłego testowania oraz wdrażania na serwery testowe i produkcyjne. Stosujemy również praktykę code review, w której co najmniej dwóch kolegów musi zatwierdzić zmiany wprowadzone przez programistę w kodzie. Statyczne analizatory kodu SonarQube i JaCoCo pomagają nam utrzymać kod w czystości i zapewnić wymagany poziom pokrycia testami jednostkowymi. Wszystkie zmiany w kodzie muszą przejść przez te kontrole. Wszystkie scenariusze testowe, które są wykonywane ręcznie, później są automatyzowane.
Aby pomyślnie realizować procesy biznesowe "Markusa", musieliśmy rozwiązać szereg zadań technologicznych, o każdej po kolei.
Zadanie 1. Konieczność poziomej skalowalności systemu
Aby rozwiązać ten problem, wybraliśmy podejście mikroserwisowe do architektury. Ważne było zrozumienie obszarów odpowiedzialności serwisów. Staraliśmy się podzielić je zgodnie z operacjami biznesowymi, biorąc pod uwagę specyfikę procesów. Na przykład, przyjęcie do magazynu to operacja, która nie zdarza się często, ale ma dużą objętość; w jej trakcie należy jak najszybciej uzyskać od regulatora państwowego informacje o przyjmowanych jednostkach towaru, których w jednej dostawie może być nawet 600000, sprawdzić dopuszczalność przyjęcia tego towaru do magazynu i przekazać wszystkie niezbędne informacje do systemu automatyzacji magazynu. Z kolei wysyłka z magazynu ma znacznie większą intensywność, ale operuje niewielkimi ilościami danych.
Wszystkie serwisy realizujemy na zasadzie stateless, a nawet wewnętrzne operacje staramy się dzielić na kroki, wykorzystując tzw. self-tematy Kafka. Oznacza to, że mikroserwis wysyła wiadomość sam do siebie, co pozwala zbalansować obciążenie w bardziej zasobożernych operacjach i ułatwia utrzymanie produktu; ale o tym później.
Postanowiliśmy wyróżnić w osobne serwisy moduły interakcji zewnętrznych systemów. To pozwoliło rozwiązać problem często zmieniających się interfejsów API zewnętrznych systemów, praktycznie bez wpływu na serwisy z funkcjami biznesowymi.

Wszystkie mikroserwisy są uruchamiane w klastrze OpenShift, który rozwiązuje zarówno problem skalowalności każdego mikroserwisu, jak i pozwala nam nie używać zewnętrznych narzędzi do odkrywania usług.
Zadanie 2. Konieczność utrzymania wysokiego obciążenia oraz bardzo intensywnej wymiany danych między serwisami platformy: tylko na etapie uruchamiania projektu realizowanych jest około 600 operacji na sekundę. Oczekujemy wzrostu tej wartości do 5000 op/s w miarę podłączania obiektów handlowych do naszej platformy.
Zadanie to rozwiązano poprzez wdrożenie klastra Kafka i praktycznie pełne zrezygnowanie z synchronicznej interakcji między mikroserwisami platformy. Wymaga to bardzo starannej analizy wymagań systemowych, ponieważ nie wszystkie operacje mogą być asynchroniczne. Przy tym nie tylko przesyłamy zdarzenia przez brokera, ale również przekazujemy w wiadomości wszystkie wymagane informacje biznesowe. W ten sposób rozmiar wiadomości może wynosić nawet kilka setek kilobajtów. Ograniczenie dotyczące wielkości wiadomości w Kafka wymaga od nas dokładnego prognozowania ich rozmiaru, a w razie potrzeby dzielimy je, jednak podział ten jest logiczny i związany z operacjami biznesowymi.
Na przykład towar przyjeżdżający w samochodzie dzielimy na kartony. Dla operacji synchronicznych wydzielane są osobne mikroserwisy, a przeprowadzane są szczegółowe testy obciążeniowe. Wykorzystanie Kafka postawiło przed nami inne wyzwanie — testowanie działania naszego serwisu z uwzględnieniem integracji Kafka czyni wszystkie nasze testy jednostkowe asynchronicznymi. W tym celu stworzyliśmy własne narzędzia przy użyciu Embedded Kafka Broker. Nie znosi to jednak konieczności pisania testów jednostkowych dla poszczególnych metod, ale skomplikowane przypadki wolimy testować przy użyciu Kafka.
Zwróciliśmy szczególnie dużo uwagi na śledzenie logów, aby ich TraceId nie zostawały utracone podczas występowania wyjątków w trakcie pracy serwisów lub podczas pracy z batchami Kafka. I jeśli w przypadku pierwszego nie było większych problemów, to w drugim przypadku musimy zapisać w logu wszystkie TraceId, które dotarły z batch, i wybrać jedno, aby kontynuować śledzenie. Wtedy przy wyszukiwaniu po początkowym TraceId użytkownik łatwo znajdzie, z którym kontynuowano śledzenie.
Zadanie 3. Konieczność przechowywania dużej ilości danych: ponad 1 miliard oznaczeń rocznie, tylko dla tytoniu, trafia do X5. Wymaga to stałego i szybkiego dostępu. Cały system powinien przetwarzać około 10 miliardów wpisów dotyczących historii ruchu danych towarów oznakowanych.
Dla rozwiązania trzeciego zadania wybrano bazę NoSQL MongoDB. Zbudowaliśmy shard z 5 węzłów, a w każdym węźle znajduje się Replica Set z 3 serwerami. Pozwala to na poziome skalowanie systemu, dodając nowe serwery do klastra i zapewnić jego odporność na awarie. Tutaj napotkaliśmy inny problem — zapewnienie transakcyjności w klastrze Mongo z uwzględnieniem użycia poziomo skalowalnych mikroserwisów. Na przykład, jedno z zadań naszego systemu — wykrywanie prób ponownej sprzedaży towarów o tych samych kodach oznakowania. Pojawiają się tutaj problemy z błędnymi skanowaniami lub błędnymi operacjami kasjerów. Odkryliśmy, że takie duplikaty mogą występować zarówno w obrębie jednego przetwarzanego batcha Kafka, jak i wśród dwóch równolegle przetwarzanych batchy. W ten sposób weryfikacja pod kątem pojawienia się duplikatów za pomocą zapytania do bazy nic nie dawała. Dla każdego z mikroserwisów rozwiązaliśmy problem osobno, opierając się na logice biznesowej danego serwisu. Na przykład, dla paragonów dodaliśmy weryfikację wewnątrz batcha i oddzielną obróbkę pod kątem pojawienia się duplikatów przy wstawianiu.
Aby działanie użytkowników z historią operacji w żaden sposób nie wpływało na najważniejsze — funkcjonowanie naszych procesów biznesowych, wszystkie dane historyczne wydzieliliśmy do oddzielnego serwisu z oddzielną bazą danych, który również otrzymuje informacje poprzez Kafka. W ten sposób użytkownicy pracują z izolowanym serwisem, nie wpływając na serwisy przetwarzające dane dotyczące bieżących operacji.
Zadanie 4. Ponowna obróbka kolejek i monitorowanie:
W systemach rozproszonych nieuchronnie pojawiają się problemy i błędy dostępności baz danych, kolejek, zewnętrznych źródeł danych. W przypadku „Markusa” źródłem takich błędów jest integracja z zewnętrznymi systemami. Konieczne było znalezienie rozwiązania, które umożliwiłoby ponowne wysyłanie zapytań w przypadku błędnych odpowiedzi z zadanym timeoutem, ale jednocześnie nie przerywało przetwarzania udanych zapytań w głównej kolejce. W tym celu wybrano tzw. koncepcję „retry oparte na tematach”. Dla każdego głównego tematu tworzony jest jeden lub więcej tematów retry, do których kierowane są błędne wiadomości, przy czym wyklucza się opóźnienia w przetwarzaniu wiadomości z głównego tematu. Schemat interakcji —

Aby zrealizować ten schemat, potrzebowaliśmy zintegrować to rozwiązanie ze Spring i uniknąć duplikacji kodu. W sieci natknęliśmy się na podobne rozwiązanie oparte na Spring BeanPostProcessor, ale wydało nam się zbyt skomplikowane. Nasz zespół opracował prostsze rozwiązanie, które pozwala na wbudowanie się w cykl Spring tworzenia konsumentów i dodatkowe dodawanie konsumentów ponownej próby. Prototyp naszego rozwiązania zaproponowaliśmy zespołowi Spring, można go zobaczyć . Liczba konsumentów ponownej próby i liczba prób każdego konsumenta są konfigurowane za pomocą parametrów, w zależności od potrzeb procesów biznesowych, a aby wszystko działało, wystarczy dodać znaną wszystkim programistom Spring adnotację org.springframework.kafka.annotation.KafkaListener.
W przypadku gdy wiadomość nie mogła być przetworzona po wszystkich próbach ponownych, trafia do DLT (dead letter topic) za pomocą Spring DeadLetterPublishingRecoverer. Na prośbę wsparcia rozszerzyliśmy tę funkcjonalność i stworzyliśmy osobną usługę, która umożliwia przeglądanie wiadomości, które trafiły do DLT, stackTrace, traceId i innych przydatnych informacji o nich. Dodatkowo dodano monitorowanie i alerty dla wszystkich topików DLT, a obecnie pojawienie się wiadomości w topiku DLT jest powodem do przeanalizowania sytuacji i zgłoszenia defektu. To bardzo wygodne — na podstawie nazwy topika od razu rozumiemy, na którym etapie procesu wystąpił problem, co znacznie przyspiesza poszukiwanie jego pierwotnej przyczyny.

Niedawno zrealizowaliśmy interfejs, który pozwala na ponowne wysyłanie wiadomości przez nasze wsparcie po usunięciu ich przyczyn (na przykład przywrócenie działania zewnętrznego systemu) i, oczywiście, zgłoszenie odpowiedniego defektu do analizy. Tu przydały się nasze self-topiki, aby nie restartować długiego łańcucha przetwarzania, można go wznowić od potrzebnego kroku.

Eksploatacja platformy
Platforma jest już w produkcyjnej eksploatacji, codziennie przeprowadzamy dostawy i wysyłki, podłączamy nowe centra dystrybucyjne i sklepy. W ramach pilota system działa z grupami towarów „Tytoń” i „Obuwie”.
Cały nasz zespół uczestniczy w przeprowadzaniu pilotaży, analizuje pojawiające się problemy i wnosi sugestie dotyczące ulepszeń naszego produktu, od poprawy logów po zmiany w procesach.
Aby nie powtarzać swoich błędów, wszystkie przypadki znalezione w trakcie pilotażu odzwierciedlają się w testach automatycznych. Duża liczba testów automatycznych i testów jednostkowych pozwala na przeprowadzanie testów regresyjnych i wprowadzanie poprawek w dosłownie kilka godzin.
Obecnie kontynuujemy rozwój i doskonalenie naszej platformy, nieustannie napotykając nowe wyzwania. Jeśli jesteś zainteresowany, opowiemy o naszych rozwiązaniach w kolejnych artykułach.
Źródło: habr.com
