Cześć! Nazywam się Wadim Madison, kieruję rozwojem System Platform w Avito. O tym, jak w firmie przechodzimy z architektury monolitycznej na mikroserwisy, mówiło się już nie raz. Czas podzielić się tym, jak przekształciliśmy naszą infrastrukturę, aby maksymalnie wykorzystać mikroserwisy i nie zagubić się w nich. Jak w tym pomaga nam PaaS, jak uprościliśmy wdrażanie i sprowadziliśmy tworzenie mikroserwisu do jednego kliknięcia — czytaj dalej. Nie wszystko, o czym piszę poniżej, zostało w Avito w pełni zrealizowane, część to sposób, w jaki rozwijamy naszą platformę.
(A na końcu tego artykułu opowiem o możliwości udziału w trzydniowym seminarium prowadzonym przez eksperta w dziedzinie architektury mikroserwisów, krisa Richardsona).

Jak doszliśmy do mikroserwisów
Avito to jeden z największych serwisów ogłoszeniowych na świecie, codziennie publikowanych jest na nim ponad 15 mln nowych ogłoszeń. Nasz backend obsługuje ponad 20 tys. zapytań na sekundę. Obecnie mamy kilkaset mikroserwisów.
Mikroserwisową architekturę budujemy od kilku lat. Jak dokładnie — nasi koledzy opowiedzą o szczegółach na naszej sesji na RIT++ 2017. Na CodeFest 2017 (zob. ), Sergey Orłow i Michaił Prokopczuk szczegółowo wyjaśnili, po co w ogóle potrzebowaliśmy przejścia na mikroserwisy i jaką rolę odegrał w tym Kubernetes. Teraz robimy wszystko, aby zminimalizować te koszty skalowania, które są charakterystyczne dla tej architektury.
Początkowo nie tworzyliśmy ekosystemu, który wszechstronnie wspierałby nas w rozwoju i uruchamianiu mikroserwisów. Po prostu zbieraliśmy sensowne rozwiązania open-source, uruchamialiśmy je u siebie i proponowaliśmy programiście z nimi się zapoznać. W efekcie chodził do dziesięciu miejsc (dashboardy, serwisy wewnętrzne), co utwierdzało go w przekonaniu, że lepiej pisać kod po staremu, w monolicie. Zielonym kolorem na poniższych schematach zaznaczone jest to, co programista w pewien sposób robi samodzielnie, żółtym kolorem — automatyzacja.

Obecnie w CLI-ubezpieczonym PaaS za pomocą jednej komendy tworzony jest nowy serwis, a dwoma dodatkowymi dodawana jest nowa baza danych i wdrażana w Stage.

Jak przetrwać erę «fragmentacji mikroserwisów»
W architekturze monolitycznej, dla spójności zmian w produkcie, deweloperzy musieli rozwiązywać, co dzieje się u sąsiadów. W nowej architekturze konteksty serwisów przestały od siebie zależeć.
Ponadto, aby architektura mikroserwisowa była skuteczna, konieczne jest wdrożenie wielu procesów, a mianowicie:
• logowanie;
• śledzenie zapytań (Jaeger);
• agregacja błędów (Sentry);
• statusy, wiadomości, zdarzenia z Kubernetes (Przetwarzanie strumieni zdarzeń);
• ograniczenie wyścigu / wyłącznik obwodu (można użyć Hystrix);
• kontrola spójności serwisów (używamy Netramesh);
• monitorowanie (Grafana);
• kompilacja (TeamCity);
• komunikacja i powiadomienia (Slack, email);
• śledzenie zadań (Jira);
• tworzenie dokumentacji.
Aby system nie stracił integralności i pozostawał efektywny w miarę skalowania, na nowo przemyśleliśmy organizację pracy mikroserwisów w Avito.
Jak zarządzamy mikroserwisami
Pomagają nam prowadzić jednolitą „politykę partii” wśród licznych mikroserwisów Avito:
- podział infrastruktury na warstwy;
- koncepcja Platform as a Service (PaaS);
- monitorowanie wszystkiego, co się dzieje z mikroserwisami.
Poziomy abstrakcji infrastruktury obejmują trzy warstwy. Przejdźmy od górnej do dolnej.
A. Górna — service mesh. Początkowo próbowaliśmy Istio, ale okazało się, że wykorzystuje zbyt dużo zasobów, co przy naszych wolumenach staje się zbyt drogie. Dlatego starszy inżynier w zespole architektury, Aleksandr Lukjanienko, opracował własne rozwiązanie — (dostępne w Open Source), które obecnie używamy w produkcji i które zużywa kilkakrotnie mniej zasobów niż Istio (ale też nie robi wszystkiego, czym może się pochwalić Istio).
B. Średnia — Kubernetes. Na nim wdrażamy i eksploatujemy mikroserwisy.
C. Dolna — bare metal. Nie korzystamy z chmur ani z rozwiązań typu OpenStack, a całkowicie opieramy się na bare metal.
Wszystkie warstwy łączą się w PaaS. A ta platforma składa się z trzech części.
I. Generatory, zarządzane przez narzędzie CLI. To właśnie ono pomaga deweloperowi stworzyć mikroserwis w właściwy sposób i z minimalnym wysiłkiem.
II. Zbiorczy kolektor z kontrolą wszystkich narzędzi przez wspólny pulpit nawigacyjny.
III. Magazyn. Integruje się z planerami, które automatycznie ustawiają wyzwalacze na znaczące działania. Dzięki takiemu systemowi żadna zadanie nie jest pomijane tylko dlatego, że ktoś zapomniał ustawić sobie zadania w Jira. Używamy do tego wewnętrznego narzędzia o nazwie Atlas.

Wdrożenie mikrousług w Avito również odbywa się według jednolitego schematu, co upraszcza kontrolę nad nimi na każdym etapie rozwoju i wydania.
Jak działa standardowy proces tworzenia mikrousługi
W ogólnym ujęciu, łańcuch tworzenia mikrousługi wygląda następująco:
CLI-push → Integracja ciągła → Bake → Wdrożenie → Testy sztuczne → Testy typu Canary → Testy Squeeze → Produkcja → Utrzymanie.
Przejdźmy przez nią dokładnie w tej sekwencji.
CLI-push
• Tworzenie mikrousługi.
Długo staraliśmy się nauczyć każdego programistę tworzyć mikrousługi. W tym celu pisaliśmy szczegółowe instrukcje w Confluence. Jednak schematy były zmieniane i uzupełniane. Efekt – wąskie gardło powstało na początku procesu: uruchomienie mikrousług zajmowało znacznie więcej czasu, niż to akceptowalne, i mimo to przy ich tworzeniu często pojawiały się problemy.
Ostatecznie stworzyliśmy prostą narzędzie CLI, które automatyzuje podstawowe kroki przy tworzeniu mikrousługi. W rzeczywistości zastępuje ono pierwsze przesłanie git. Oto, co konkretnie robi.
— Tworzy usługę na podstawie szablonu — krok po kroku, w trybie „czarodzieja”. Mamy szablony dla głównych języków programowania w backendzie Avito: PHP, Golang i Python.
— Z jedną komendą uruchamia środowisko do lokalnego rozwoju na konkretnej maszynie — uruchamia Minikube, szablony Helm są automatycznie generowane i uruchamiane w lokalnym Kubernetesie.
— Podłącza potrzebną bazę danych. Programista nie musi znać IP, loginu i hasła, aby uzyskać dostęp do potrzebnej mu bazy danych — zarówno lokalnie, w Stage, jak i na produkcji. Co więcej, baza danych jest natychmiast wdrażana w konfiguracji odpornej na awarie oraz z równoważeniem obciążenia.
— Samodzielnie wykonuje live-build. Na przykład, jeśli programista coś poprawił w mikrousłudze przez swoją IDE, narzędzie widzi zmiany w systemie plików i na ich podstawie przetwarza aplikację (dla Golang) i restartuje. Dla PHP po prostu przekazujemy katalog do kubu, a tam live-reload odbywa się automatycznie.
— Generuje automatyczne testy. W formie szablonów, ale w pełni użytecznych.
• Wdrażanie mikroserwisu.
Wdrażanie mikroserwisu było u nas wcześniej dość uciążliwe. Wymagane były obowiązkowo:
I. Dockerfile.
II. Konfiguracja.
III. Helm chart, który sam w sobie jest obszerny i zawiera:
— same chartry;
— szablony;
— konkretne wartości z uwzględnieniem różnych środowisk.
Odetchnęliśmy od bólu przekształcania manifestów Kubernetes, a teraz są one generowane automatycznie. Ale najważniejsze, uprościliśmy do minimum wdrażanie. Od teraz mamy Dockerfile, a cała konfiguracja jest opisana przez programistę w jednym krótkim pliku app.toml.

A w samym app.toml teraz sprawa zajmuje minutę. Opisujemy, ile kopii usługi uruchomić (na serwerze deweloperskim, na stagingu, na produkcji), wskazujemy jej zależności. Zwróć uwagę na linię size = „small” w bloku [engine]. To limit, który będzie przydzielony usłudze przez Kubernetes.
Następnie na podstawie konfiguracji automatycznie generowane są wszystkie niezbędne Helm charty i tworzone są połączenia z bazami danych.
• Podstawowa walidacja. Takie kontrole również są zautomatyzowane.
Należy śledzić:
— czy istnieje Dockerfile;
— czy istnieje app.toml;
— czy istnieje dokumentacja;
— czy zależności są w porządku;
— czy określone są zasady alertów.
Co do ostatniego punktu: właściciel usługi sam wskazuje, jakie metryki produktowe monitorować.
• Przygotowanie dokumentacji.
Wciąż problemowe miejsce. Wydaje się oczywiste, ale jednocześnie jest to rekordowo „często zapominany” element, a zatem i wrażliwe ogniwo w łańcuchu.
Dokumentacja musi być dla każdego mikroserwisu. Wchodzi w nią następujące bloki.
I. Krótkie opisanie usługi. Dosłownie kilka zdań na temat tego, co robi i do czego jest potrzebna.
II. Link do diagramu architektury. Ważne, aby przy pobieżnym spojrzeniu można było łatwo zrozumieć, na przykład, czy używacie Redis do cachowania, czy jako głównego magazynu danych w trybie trwałym. W Avito to nadal jest link do Confluence.
III. Runbook. Krótkie wskazówki dotyczące uruchamiania usługi i szczegóły dotyczące jej obsługi.
IV. FAQ, w którym dobrze byłoby przewidzieć problemy, z którymi mogą się zmierzyć Twoi koledzy podczas pracy z usługą.
V. Opis endpointów dla API. Jeśli nie określiłeś punktów końcowych, prawdopodobnie zapłacą za to koledzy, których mikroserwisy są powiązane z Twoim. Obecnie wykorzystujemy do tego Swagger i nasze rozwiązanie o nazwie brief.
VI. Etykiety. Albo znaczniki, które pokazują, do jakiego produktu, funkcjonalności, czy też strukturalnej jednostki firmy odnosi się serwis. Pomagają szybko zrozumieć, na przykład, czy nie rozwijasz funkcjonalności, którą tydzień temu wdrożyli Twoi koledzy dla tego samego działu biznesowego.
VII. Właściciel lub właściciele serwisu. W większości przypadków jego — lub ich — można określić automatycznie za pomocą PaaS, ale dla bezpieczeństwa wymagamy od dewelopera, aby wskazał ich ręcznie.
Wreszcie, dobrą praktyką jest przeprowadzanie przeglądu dokumentacji, na wzór przeglądu kodu.
i pozwala uprościć przygotowanie kodu do wdrożenia.
- Przygotowanie repozytoriów.
- Tworzenie procesu w TeamCity.
- Przyznawanie uprawnień.
- Wyszukiwanie właścicieli serwisu. To hybrydowy schemat — ręczne etykietowanie i minimalna automatyka od PaaS. Całkowicie automatyczny schemat zawodzi przy przekazywaniu usług do wsparcia innej drużynie deweloperskiej lub, na przykład, jeśli deweloper serwisu odszedł.
- Rejestracja serwisu w Atlasie (zob. powyżej). Z wszystkimi jego właścicielami i zależnościami.
- Sprawdzanie migracji. Sprawdzamy, czy nie ma wśród nich potencjalnie niebezpiecznych. Na przykład w jednej z nich pojawia się alter table lub inne coś, co może naruszyć zgodność schematu danych między różnymi wersjami serwisu. W takim przypadku migracja nie jest wykonywana, a umieszczana na subskrypcji — PaaS musi powiadomić właściciela serwisu, kiedy będzie bezpiecznie ją zastosować.
Bake
Następny etap — pakowanie usług przed wdrożeniem.
- Budowanie aplikacji. Klasycznie — w obrazie Docker.
- Generowanie chartów Helm dla samego serwisu i powiązanych z nim zasobów. W tym dla baz danych i pamięci podręcznych. Są one tworzony automatycznie zgodnie z tym konfiguracją app.toml, która została utworzona na etapie CLI-push.
- Tworzenie zgłoszeń do administratorów w celu otwarcia portów (gdy jest to wymagane).
- Uruchamianie testów jednostkowych i obliczanie pokrycia kodu.. Jeśli pokrycie kodu spada poniżej określonego progu, to prawdopodobnie dalsza część procesu, jak wdrożenie, nie przejdzie. Jeśli będzie bliskie dopuszczalnego poziomu, usługa otrzyma wskaźnik „pesymistyczny”: wtedy, w przypadku braku poprawy wskaźnika z upływem czasu, programista otrzyma powiadomienie, że nie ma postępu w kwestii testów (i należy coś z tym zrobić).
- Uwzględnienie ograniczeń pamięci i CPU. Głównie mikrousługi piszemy w Golangu i uruchamiamy je w Kubernetesie. Stąd wynika jedna subtelność związana z językiem Golang: domyślnie, przy uruchomieniu, wykorzystane są wszystkie rdzenie w maszynie, chyba że jawnie ustawi się zmienną GOMAXPROCS, a gdy na jednej maszynie uruchamiane jest kilka takich usług, zaczynają one konkurować o zasoby, przeszkadzając sobie nawzajem. Na poniższych wykresach pokazano, jak zmienia się czas wykonania, jeśli uruchomimy aplikację bez konkurencji i w trybie rywalizacji o zasoby. (Źródła wykresów można znaleźć ).
Czas wykonania, im mniej tym lepiej. Maksimum: 643ms, minimum: 42ms. Zdjęcie klikane.
Czas operacji, im mniej tym lepiej. Maksimum: 14091 ns, minimum: 151 ns. Zdjęcie klikane.
Na etapie przygotowania budowy można jawnie ustawić tę zmienną lub można skorzystać z biblioteki od zespołu Ubera.
Wdrożenie
• Sprawdzanie konwencji. Zanim zaczniemy dostarczać pakiety usługi do wyznaczonych środowisk, należy sprawdzić następujące kwestie:
— Punkt końcowy API.
— Zgodność odpowiedzi punktów końcowych API z harmonogramem.
— Format logów.
— Ustawianie nagłówków при запросах к сервису (obecnie robi to netramesh)
— Ustawianie znacznika właściciela przy wysyłaniu wiadomości do magistrali (event bus). Jest to potrzebne do śledzenia powiązań usług przez magistralę. W magistralę można wysyłać zarówno idempotentne dane, które nie zwiększają powiązań usług (co jest dobre), jak i dane biznesowe, które te powiązania wzmacniają (co jest bardzo złe!). W momencie, gdy te powiązania stają się problemem, zrozumienie, kto pisze i czyta magistralę, pomaga właściwie podzielić usługi.
Na razie konwencji w Avito nie jest zbyt dużo, ale ich zasób rośnie. Im więcej takich porozumień w formie zrozumiałej i wygodnej dla zespołu, tym łatwiej utrzymać zgodność między mikrousługami.
Testy syntetyczne
• Testowanie w zamkniętej pętli. Obecnie używamy opensourcowego . Najpierw rejestruje rzeczywiste obciążenie usługi, a następnie - właśnie w zamkniętej pętli - je emuluje.
• Testowanie obciążeniowe. Staramy się dostosować wszystkie usługi do optymalnej wydajności. Wszystkie wersje każdej usługi muszą przechodzić testy obciążeniowe - dzięki temu możemy zrozumieć bieżącą wydajność usługi oraz różnice w stosunku do poprzednich wersji tej samej usługi. Jeśli po aktualizacji wydajność usługi spada o półtora raza, to wyraźny sygnał dla jej właścicieli: trzeba zagłębić się w kod i naprawić sytuację.
Na podstawie zebranych danych, na przykład, realizujemy prawidłowo auto skalowanie i, w końcu, ogólnie rozumiemy, jak bardzo usługa poddaje się skalowaniu.
Podczas testowania obciążeniowego sprawdzamy, czy zużycie zasobów odpowiada nałożonym ograniczeniom. Skupiamy się przede wszystkim na ekstremach.
a) Patrzymy na ogólne obciążenie.
— Zbyt małe - najprawdopodobniej coś w ogóle nie działa, jeśli obciążenie nagle spadło kilkakrotnie.
— Zbyt duże - wymaga optymalizacji.
b) Patrzymy na odcięcie po RPS.
Tu patrzymy zarówno na różnicę między bieżącą a poprzednią wersją, jak i na całkowitą liczbę. Na przykład, jeśli usługa generuje 100 rps - to albo jest źle napisana, albo to jest jej specyfika, ale w każdym razie to powód, aby bardzo uważnie przyjrzeć się usłudze.
Jeśli zaś RPS jest zbyt wysoki, to być może występuje jakiś błąd i jakiś z endpointów przestał realizować obciążenie, a po prostu wywołuje jakiś return true;
testy Canary
Po przejściu testów syntetycznych, sprawdzamy działanie mikroserwisu na niewielkiej liczbie użytkowników. Zaczynamy ostrożnie, od minimalnego udziału przewidywanej publiczności usługi - mniej niż 0,1%. Na tym etapie bardzo ważne jest, aby w monitorowaniu były wprowadzone właściwe metryki techniczne i produktowe, aby jak najszybciej pokazały problem w usłudze. Minimalny czas testu canary to 5 minut, a główny - 2 godziny. Dla skomplikowanych usług ustawiamy czas ręcznie.
Analizujemy:
— metryki specyficzne dla języka, w szczególności, workerzy php-fpm;
— błędy w Sentry;
— statusy odpowiedzi;
— czasy odpowiedzi (response time), dokładne i średnie;
— opóźnienia;
— wyjątki, obsłużone i nieobsłużone;
— metryki produktowe.
Testowanie wyciskające
Testy Squeeze nazywane są również testowaniem przez „wyciskanie”. Nazwę tej metody wprowadzono w Netflix. Jej istota polega na tym, że najpierw wypełniamy jeden instans rzeczywistym ruchem do momentu awarii, a tym samym ustalamy jego limit. Następnie dodajemy kolejny instans i obciążamy tę parę — znów do maksimum; widzimy ich sufit i różnicę w stosunku do pierwszego „wyciśnięcia”. I tak podłączamy po jednym instansie na krok i obliczamy regularności w zmianach.
Dane o testach związanych z „wyciskaniem” również spływają do wspólnej bazy metryk, gdzie zarówno wzbogacamy nimi wyniki sztucznego obciążenia, jak i całkowicie zastępujemy nimi „syntetykę”.
Produkcja
• Skalowanie. Wprowadzając usługę na produkcję, śledzimy, jak się ona skaluje. Monitorowanie tylko wskaźników CPU, według naszego doświadczenia, jest nieskuteczne. Auto scaling z benchmarkingiem RPS w czystej postaci działa, ale tylko dla wybranych usług, na przykład streamingu online. Dlatego przede wszystkim zwracamy uwagę na specyficzne dla aplikacji metryki produktowe.
W rezultacie, przy skalowaniu analizujemy:
— wskaźniki CPU i RAM,
— liczbę zapytań w kolejce,
— czas odpowiedzi,
— prognozę na podstawie zgromadzonych danych historycznych.
Podczas skalowania usługi ważne jest również monitorowanie jej zależności, aby nie zdarzyło się, że skalujemy pierwszy serwis w łańcuchu, a te, do których się on odnosi, zawiodą pod obciążeniem. Aby ustalić akceptowalne obciążenie dla całego zestawu usług, analizujemy dane historyczne „najbliższego” serwisu zależnego (biorąc pod uwagę kombinację wskaźników CPU i RAM w połączeniu z metrykami specyficznymi dla aplikacji) i porównujemy je z danymi historycznymi inicjującego serwisu, i tak dalej w całym „łańcuchu zależności”, od góry do dołu.
Usługa
Po wprowadzeniu mikroserwisu do eksploatacji możemy nałożyć na niego wyzwalacze.
Oto typowe sytuacje, w których uruchamiane są wyzwalacze.
— Wykryto potencjalnie niebezpieczne migracje.
— Wydano aktualizacje zabezpieczeń.
— Sam serwis od dłuższego czasu nie był aktualizowany.
— Zdecydowanie zmniejszyło się obciążenie serwisu lub jakieś jego metryki produktowe wykroczyły poza normę.
— Serwis przestał spełniać nowe wymagania platformy.
Część triggerów odpowiada za stabilność działania, część — jako funkcja serwisowania systemu — na przykład, jakiś serwis nie był od dawna wdrożony i jego podstawowy obraz przestał przechodzić testy bezpieczeństwa.
Panel kontrolny
Mówiąc krótko, dashboard to kontrolny panel całego naszego PaaS.
- Jednolity punkt informacji o serwisie, z danymi na temat jego pokrycia testami, liczby jego obrazów, liczby kopii produkcyjnych, wersji itd.
- Narzędzie do filtrowania danych według serwisów i etykiet (markerów przynależności do jednostek biznesowych, funkcjonalności produktu itd.)
- Narzędzie integracji z narzędziami infrastrukturalnymi do śledzenia, logowania, monitorowania.
- Jednolity punkt dokumentacji dla serwisów.
- Jednolity punkt przeglądu wszystkich zdarzeń dotyczących serwisów.




Podsumowując
Przed wdrożeniem PaaS nowy programista mógł spędzić kilka tygodni na zapoznaniu się ze wszystkimi narzędziami potrzebnymi do uruchomienia mikroserwisu w produkcji: Kubernetes, Helm — w naszych wewnętrznych specyfikach TeamCity, konfigurowaniu połączeń z bazami i pamięciami podręcznymi w sposób odporny na błędy itd. Teraz zajmuje to tylko kilka godzin — przeczytać quickstart i stworzyć sam serwis.
Zrobiłem na ten temat prezentację dla HighLoad++ 2018, można zobaczyć. i .
Bonusowy utwór dla tych, którzy dotrwali do końca.
Organizujemy wewnętrzne, trzydniowe szkolenie dla programistów w Avito od , eksperta w dziedzinie architektury mikroserwisowej. Chcemy dać możliwość udziału w nim komuś z czytelników tego posta. Program szkolenia został opublikowany.
Szkolenie odbędzie się od 5 do 7 sierpnia w Moskwie. To dni robocze, które będą w całości zajęte. Lunch i szkolenie będą w naszym biurze, a droga i zakwaterowanie opłaca uczestnik.
Aby zgłosić chęć udziału, można . Od Ciebie — odpowiedź na pytanie, dlaczego to właśnie Ty musisz wziąć udział w szkoleniu oraz informacja, jak się z Tobą skontaktować. Odpowiadaj po angielsku, ponieważ uczestnika, który weźmie udział w szkoleniu, Chris wybierze osobiście.
Ogłosimy imię uczestnika szkolenia jako aktualizację do tego posta oraz w mediach społecznościowych Avito dla programistów (AvitoTech w , , ) najpóźniej do 19 lipca.
Źródło: habr.com
