Jak monitorować rozproszone mikroserwisy?
Wyobraź sobie, że dostajesz zgłoszenie, że na produkcji wystąpił błąd. Zespół, który korzysta z Twojego rozwiązania, wysłał żądanie i operacja zawiodła - trwała zbyt długo i został zwrócony błąd. Podają requestId, datę i szczegóły żądania. Twój system to sieć mikrousług wdrożona w środowisku rozproszonym. Czy jesteś w stanie szybko odpowiedzieć, co konkretnie było przyczyną błędu?
W tym wpisie porozmawiamy o Observability (obserwowalności) systemów opartą o architekturę rozproszonych mikrousług. Pokażę, jakie są rodzaje narzędzi do monitorowania i jak mogą Ci pomóc w rozwiązywaniu sytuacji, takich jak:
- Log aggregation
- Distributed Tracing
- Monitoring
Sprawi to, że będziesz dobrze monitorował swój system i nie będziesz niewidomy na szereg problemów. Zaczynajmy.
Log aggregation
Pierwszym miejscem, w które można zajrzeć to logi aplikacji. W nich znajdziesz przebieg żądania, które przyszło do systemu. To, co warto zanotować w logu to:
- dane wejściowe (request) do naszego systemu,
- zwróconą odpowiedź,
- kroki pośrednie w logice lub obliczeniach, np. niektóre dane z repozytoriów danych lub cache, które zostały uwzględnione do podjęcia decyzji,
- dane wejściowe i odpowiedzi do innych mikrousług, jeżeli są wywoływane,
- RequestId / CorrelationId.
- data i czas, jeżeli aplikacja działa w wielu regionach, to czas UTC.
RequestId to identyfikator żądania do naszego systemu, który możemy wygenerować na czas przetwarzania. Posłuży on do skorelowania wszystkich linijek logu w ramach jednego przetwarzanego żądania.
CorrelationId to identyfikator całej operacji nadawany w systemie źródłowym i przekazywany przez wszystkie aplikacje. Najczęściej dzieje się to za pomocą nagłówków. Służy on do skorelowania logów ze wszystkich aplikacji, przez które przeszedł request od początku do końca (front-to-back).
Takie identyfikatory możemy przechowywać w kontekście wykonywania żądania. Dla Javy może być to MDC Context (dostępny w ramach slf4j i implementacjach) lub Reactor Context.
Przykładowa konfiguracja logowania Logback dla Javy może wyglądać tak:
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date{"yyyy-MM-dd HH:mm:ss,SSSXXX", UTC} {%X{contextId}} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
Co skutkuje wpisem:
2020-05-28 07:21:51,081Z {c5b83992-1da1-461d-86ba-707463727b00} [http-nio-8080-exec-2] INFO p.s.c.t.b.r.i.LoggingFilter - Before request [POST /basket/d0a56359-ee77-4e62-a078-a4d1d66ed81b/summary]
2020-05-28 07:21:51,184Z {c5b83992-1da1-461d-86ba-707463727b00} [http-nio-8080-exec-2] INFO p.s.c.t.b.r.c.BasketController - Getting information about basket d0a56359-ee77-4e62-a078-a4d1d66ed81b
// ...
Tak zorganizowane logi można bardzo łatwo przeszukać po RequestId.
Agregowanie logów
Jeżeli system jest wdrożony w rozproszonym, auto skalowanym środowisku, nikt nie będzie się logował na poszczególne maszyny i przeszukiwał logów. Po pierwsze jest to czasochłonne. Po drugie, w efemerycznym środowisku kontenery i związane z nimi dane mogą już nie istnieć.
Z pomocą przychodzą systemy Log Aggregation. Służą one do zbierania logów z wielu źródeł w jedno miejsce. To miejsce można łatwo przeszukać, np. po RequestId. Wtedy otrzymamy wszystkie logi dotyczące tego żądania, nieważne gdzie się wykonały.
Systemy Log aggregation działają w kilku modelach:
Wysyłanie wpisów logów bezpośrednio z aplikacji
To najprostszy sposób, polega na dodaniu dodatkowego loggera, który wysyła wpisy do systemu Log Aggregation. Przykładem może być Gelf (Graylog Extended Log Format).
Przykładowa konfiguracja Logback i Gelf:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="CentralizedLoggingGelf" class="de.siegmar.logbackgelf.GelfTcpAppender">
<param name="graylogHost" value="localhost"/>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
<appender-ref ref="CentralizedLoggingGelf" />
</root>
</configuration>
Należy uważać, aby ten logger podczas komunikacji z systemem Log Aggregation nie spowolnił aplikacji w momencie niedostępności. Zalecam asynchroniczne wysyłanie logów i niereagowanie na błędy systemu Log Aggregation.
Agenty monitorujące
Powyższy problem z blokowaniem się aplikacji w momencie logowania rozwiązują agenty monitorujące logi aplikacji. Agent zainstalowany jest na tej samej maszynie, co aplikacja i obserwuje logi przez nią tworzone. Nie obciąża to aplikacji, ponieważ proces działa obok niej.
Przykładami takich rozwiązań są Fluentd, Splunk lub Graylog/Logstash.
Sidecars
Innym podejściem jest zainstalowanie agenta monitorującego jako sidecar container. Aplikacja uruchomiona w kontenerze współdzieli z kontenerem agenta monitorującego katalog z logami. Agent niezależnie od aplikacji śledzi logi i wysyła je do systemu Log Aggregation.
Do sidecar container można umieścić dowolnego agenta. Przykładem tego typu rozwiązania jest np. Fluend, Graylog, Logstash.
Przykłady systemów log aggregation:
- ELK - stos oparty o ElasticSearch (baza danych), Logback i Beats (przekazywanie logów) i Kibana (wyświetlanie)
- Graylog
- Fluentd - zbiera dane z wielu źródeł i wysyła do różnych baz (np. ElasticSearch, MongoDB, Hadoop, chmury)
- Splunk - kompleksowe rozwiązanie do analizy danych, przesyłanie logów aplikacji do centralnego serwera i tworzenie z logów raportów jako Low-Code
- Oferowane przez Twoją chmurę - GCP Cloud Logging, Amazon CloudWatch, Azure Monitor.
Tracing - śledzenie konkretnego żądania
Distributed Tracing to śledzenie ścieżki żądania, gdy przechodzi ono przez różne części naszego systemu pomiędzy aplikacjami. Analizując ścieżkę zauważysz:
- Który system w tym konkretnym przypadku (requeście) spowodowało opóźnienie
- Jeżeli zawiodła przepustowość sieci, zauważysz, że żądanie utknęło pomiędzy systemami.
Gdy wysyłane jest żądanie, w pierwszym systemie generowany jest TraceId. Jest to identyfikator całej ścieżki.
Gdy wykonuje się jakiś kod, jego czas jest mierzony w ramach Span-ów. Są to zakresy czasowe, oznaczają początek wykonywania i koniec wykonywania kodu. Każdy Span ma swój identyfikator SpanId, referencję do TraceId oraz opcjonalnie referencję do nadrzędnego SpanId.
Mierzonymi fragmentami może być wywoływania baz danych, innych serwisów, inne operacje I/O oraz wykonywanie kodu.
Distributed Tracing różni się od zwykłych timerów tym, że znamy kontekst wywołania, a dane zbierane są z różnych aplikacji do centralnego rejestru.
Spany mogą być zagnieżdżone, dzięki czemu możemy otrzymać drzewo wywołań z informacją, która operacja wynika z jakiej i ile zajęła czasu:
Już na pierwszy rzut oka wiedać:
- Jaki jest przebieg żądania pomiędzy systemami,
- Ile czasu zajęła obsługa żądania i w jakim miejscu
- Ile czasu żądanie spędziło na komunikacji.
TraceId, SpanId oraz ParentSpanId tworzą tzw. Tracing Context, czyli kontekst śledzenia żądania. Aby przekazać kontekst pomiędzy aplikacjami stosuje się nałówki. Powstał standard, który określa ich nazwę oraz przechowywane wartości: B3 Propagation Specification. Te informacje można umieścić w metadanych używanych w sposobie komunikacji, na przykład w requestach HTTP lub wiadomościach systemach kolejkowych (JMS, Apache Kafka) będą to nagłówki: headery.
Umieszczanie nagłówków w każdym wywołaniu serwisu lub wiadomości w systemach kolejkowych jest uciążliwe, trzeba o tym pamiętać. Można wpiąć się z różnego rodzaju hookami, interceptorami, listenerami - przed i po wysłaniu requesta w różnych bibliotekach.
Nie trzeba tego zaprogramować samemu. W ekosystemie Java powstała biblioteka do instrumentacji kodu Brave, która implementuje taką instrumentację kodu dla najpopularniejszych bibliotek używanych przez programistów.
Dodatkowo, jeżeli używasz Spring Boot, możesz wykorzystać projekt Spring Cloud Sleuth. Używa on biblioteki Brave do instrumentacji beanów stworzonych przez Ciebie oraz przez framework. Dekoruje je powodując, że są gotowe do użycia.
Konkretnymi rozwiązaniami Tracingu są:
- Zipkin
- Jaeger
- Dynatrace
- Elastic APM
Do integracji z systemami tracingowymi możesz też wykorzystać standard OpenTracing, a teraz opracowywany jest standard OpenTelementry.
Monitorowanie kondycji systemu
Log Aggregation i Distributed Tracing pomoże Ci zidentyfikować problemy z pojedynczymi requestami. Warto jednak jest monitorować ogólną kondycję systemu. Na różnego rodzaju dashboardach możesz umieścić zagregowane dane na temat metryk (średnie, mediany, percentyle), na przykład czas obsługi żądań, interakcji z bazą danych, innymi serwisami.
Przydadzą Ci się dwa rodzaje dashboardów: ogólny per cała mikrousługa i szczegółowy per instancja.
Z monitoringu otrzymasz odpowiedzi na hipotezy:
- Od czasu T1 do czasu T2 obserwowaliśmy wydłużone czasy odpowiedzi. Było to spowodowane wzrostem czasu odpowiedzi z bazy danych.
- Przez moment serwis X odpowiadał wolniej.
- Od ostatniego wdrożenia czasy obsługi spadły. Trzeba zobaczyć, co konkretnie się stało.
- Ta konkretna instancja serwisu odpowiada zawsze wolniej, bo maszyna na której działa ma wysycone CPU.
Perspektywa ogólna
Perspektywa ogólna to widok na dany mikroserwis “z lotu ptaka”. Zawiera informacje w zagregowanej formie:
- Liczba działających instancji
- Liczba przychodzących żądań do całego systemu i czas odpowiedzi (mediana, percentyl 95, percentyl 99)
- Monitorowane zależności serwisu, np. Czas odpowiedzi bazy danych (ms), liczba zapytań (iops), czas odpowiedzi z innych serwisów (ms), czas publikacji do systemu kolejkowego (ms)
- Monitoring cache: liczba wpisów, hit/miss ratio (%)
Czasy odpowiedzi mogłyby być również podzielone na instancje, aby móc zidentyfikować wadliwe i przejść do drugiego dashboardu.
Perspektywa instancji
Perspektywa instancji skupia się na konkretnym działającym procesie mikrousługi. Przydatnymi metrykami będą:
- wszystkie z dashboardu ogólnego oraz otoczenie działania aplikacji:
- CPU: % zużycia ogólnego i % zużycia przez naszą aplikację
- Zużycie pamięci na maszynie
- Zużycie pamięci w aplikacji z podziałem na generacje pamięci
- Momenty i czasy działania Garbage Collector, rezultat działania (ile pamięci jest zajętej po jego działaniu).
Dzięki temu zdiagnozujesz, czy dana instancja odpowiada wolniej bo tylko ona ma problem z odpowiedziami z bazy danych i innych serwisów, czy jest to może ogólny trend (z dashboardu ogólnego). Wykryjesz potencjalne wycieki pamięci i będziesz miał pogląd na ten konkretny proces, mogąc wyciągać wnioski w nie zagregowanej formie.
Przykładami narzędzi są:
- Grafana - dashboard
- Prometheus - system zbierający metryki w trybie pull
- Graphite - jedna z baz typu RRD (round-robin database), do której możemy wysyłać metryki
- InfluxDB
- StatsD
- New Relic
- Dynatrace
- Amazon CloudWatch
- Azure Monitor
Podsumowanie
Monitorując aplikację na wielu poziomach, możesz ją diagnozować per-żądanie lub obserwować ogólną kondycję systemu.
- Log Aggregation umożliwi Ci zebranie wszystkich logów w jedno miejsce oraz wygodne ich przeszukiwanie.
- Distributed Tracing pomoże Ci prześledzić, gdzie i jak długo żądanie spędziło czas.
- Monitoring pozwoli Ci na ogólną ocenę kondycji systemu eksponując zagregowane statystyki.