
W Badoo nieustannie monitorujemy nowe technologie i oceniamy, czy warto je wdrożyć w naszym systemie. Chcielibyśmy się podzielić jednym z takich badań z naszą społecznością. Dotyczy ono Loki — systemu agregacji logów.
Loki to rozwiązanie do przechowywania i przeglądania logów, które oferuje również elastyczny system do ich analizy oraz przesyłania danych do Prometheus. W maju ukazała się kolejna aktualizacja, która jest aktywnie promowana przez twórców. Zainteresowało nas, co potrafi Loki, jakie możliwości oferuje i w jakim stopniu może być alternatywą dla ELK — stosu, którego obecnie używamy.
Co to jest Loki
Grafana Loki to zestaw komponentów do pełnoprawnego systemu pracy z logami. W przeciwieństwie do innych podobnych systemów, Loki opiera się na idei indeksowania tylko metadanych logów — labels (tak jak w Prometheus), podczas gdy same logi są kompresowane jako oddzielne kawałki.
,
Zanim przejdziemy do opisu możliwości, jakie oferuje Loki, chciałbym wyjaśnić, co rozumiemy przez "ideę indeksowania tylko metadanych". Porównajmy podejście Loki z podejściem do indeksowania w tradycyjnych rozwiązaniach, takich jak Elasticsearch, na przykładzie wpisu z logu nginx:
172.19.0.4 - - [01/Jun/2020:12:05:03 +0000] "GET /purchase?user_id=75146478&item_id=34234 HTTP/1.1" 500 8102 "-" "Stub_Bot/3.0" "0.001"Tradycyjne systemy parsują całą linię, w tym pola z dużą liczbą unikalnych wartości user_id i item_id, i zapisują wszystko w dużych indeksach. Zaletą tego podejścia jest to, że można szybko wykonywać złożone zapytania, ponieważ prawie wszystkie dane znajdują się w indeksie. Jednak wiąże się to z tym, że indeks staje się duży, co prowadzi do wymagań pamięciowych. W rezultacie pełnotekstowy indeks logów jest wielkości porównywalnej z samymi logami. Aby szybko przeszukiwać, indeks musi być załadowany w pamięci. Im więcej logów, tym szybciej rośnie indeks i tym więcej pamięci zużywa.
Podejście Loki wymaga, aby z ciągu zostały wydobyte tylko niezbędne dane, których liczba jest niewielka. W ten sposób uzyskujemy mały indeks i możemy przeszukiwać dane, filtrując je według czasu oraz zindeksowanych pól, a następnie skanując pozostałe za pomocą wyrażeń regularnych lub wyszukiwania podciągów. Proces wydaje się nie najszybszy, ale Loki dzieli zapytanie na kilka części i wykonuje je równolegle, przetwarzając dużą ilość danych w krótkim czasie. Liczba shardów i równoległych zapytań w nich jest konfigurowana; w ten sposób liczba danych, które można przetworzyć w jednostce czasu, jest liniowo zależna od liczby dostarczonych zasobów.
Ten kompromis między dużym szybkim indeksem a małym indeksem z równoległym pełnym przeszukiwaniem pozwala Loki kontrolować koszty systemu. Można go elastycznie dostosować i rozszerzać w zależności od potrzeb.
Stos Loki składa się z trzech komponentów: Promtail, Loki, Grafana. Promtail zbiera logi, przetwarza je i wysyła do Loki. Loki je przechowuje. A Grafana potrafi zapytywać dane z Loki i je wyświetlać. Ogólnie rzecz biorąc, Loki można używać nie tylko do przechowywania logów i ich przeszukiwania. Cały stos daje ogromne możliwości przetwarzania i analizy napływających danych, wykorzystując sposób Prometheusa.
Opis procesu instalacji można znaleźć .
Wyszukiwanie w logach
Wyszukiwanie w logach można przeprowadzać w specjalnym interfejsie Grafana — Explorer. Do zapytań używa się języka LogQL, bardzo podobnego do PromQL używanego w Prometheusie. W zasadzie można go rozpatrywać jako rozproszony grep.
Interfejs wyszukiwania wygląda tak:

Same zapytanie składa się z dwóch części: selector i filter. Selector to wyszukiwanie po zindeksowanych metadanych (etykietach), które są przypisane do logów, a filter to ciąg wyszukiwania lub regexp, za pomocą którego filtruje się rekordy określone przez selektor. W podanym przykładzie: W nawiasach klamrowych — selektor, wszystko, co po — filtr.
{image_name="nginx.promtail.test"} |= "index"Z powodu zasady działania Loki nie można wykonywać zapytań bez selektora, ale etykiety można czynić dowolnie ogólnymi.
Selektor to wartości key-value w nawiasach klamrowych. Można łączyć selektory i ustawiać różne warunki wyszukiwania, używając operatorów =, != lub wyrażeń regularnych:
{instance=~"kafka-[23]",name!="kafka-dev"}
// Znajdzie logi z etykietą instance, mające wartość kafka-2, kafka-3, i wykluczy dev Filtr to tekst lub wyrażenie regularne, które przefiltruje wszystkie dane uzyskane przez selektor.
Istnieje możliwość uzyskania ad-hoc wykresów na podstawie zebranych danych w trybie metrics. Na przykład, można poznać częstotliwość występowania w logach nginx wpisu zawierającego ciąg index:

Pełny opis możliwości można znaleźć w dokumentacji .
Parsowanie logów
Istnieje kilka sposobów zbierania logów:
- Za pomocą Promtail, standardowego komponentu stosu do zbierania logów.
- Bezpośrednio z kontenera dockera przy pomocy
- Można użyć Fluentd lub Fluent Bit, które potrafią wysyłać dane do Loki. W przeciwieństwie do Promtail mają gotowe parsery praktycznie do każdego rodzaju logu i radzą sobie również z logami multiline.
Zazwyczaj do parsowania używa się Promtail. Wykonuje on trzy rzeczy:
- Znajduje źródła danych.
- Przypina do nich etykiety.
- Wysyła dane do Loki.
Obecnie Promtail może czytać logi z lokalnych plików oraz z dziennika systemd. Musi być zainstalowany na każdej maszynie, z której zbierane są logi.
Jest integracja z Kubernetes: Promtail automatycznie przez Kubernetes REST API poznaje stan klastra i zbiera logi z węzła, usługi lub poda, jednocześnie przypinając etykiety na podstawie metadanych z Kubernetes (nazwa poda, nazwa pliku itp.).
Można również przypinać etykiety na podstawie danych z logu przy pomocy Pipeline. Pipeline Promtail może składać się z czterech typów etapów. Szczegóły można znaleźć , tutaj zwrócę uwagę na kilka szczegółów.
- Etapy parsowania. To etap RegEx i JSON. Na tym etapie wyciągamy dane z logów do tak zwanej extracted map. Można wyciągać z JSON, po prostu kopiując wymagane nam pola do extracted map, lub za pomocą wyrażeń regularnych (RegEx), gdzie w extracted map
- mapowane są grupy nazwane. Extracted map to magazyn key-value, gdzie key to nazwa pola, a value to jego wartość z logów.Etapy transformacji Ponadto należy pamiętać, że mapa extracted ładnie się ładowana podczas parsowania, co pozwala na przykład na sprawdzenie wartości: "{{if .tag}tag value exists{end}}". Szablon obsługuje warunki, pętle oraz niektóre funkcje tekstowe, takie jak Replace i Trim.
- Etapy działaniaNa tym etapie można coś zrobić z wypisanym:
- Utworzyć etykietę z extracted data, która zostanie zindeksowana w Loki.
- Zmień lub ustaw czas zdarzenia z logu.
- Zmień dane (tekst logu), które zostaną przesłane do Loki.
- Utwórz metryki.
- Etapy filtrowaniaEtap match, na którym można albo wysłać zapisy, które są nam niepotrzebne, do /dev/null, albo skierować je do dalszego przetwarzania.
Pokażę na przykładzie przetwarzania typowych logów nginx, jak można parsować logi za pomocą Promtail.
Na test weźmiemy zmodyfikowany obraz nginx jwilder/nginx-proxy:alpine jako nginx-proxy i małego demona, który potrafi pytać sam siebie przez HTTP. Demon ma kilka punktów końcowych, na które może przekazywać odpowiedzi różnej wielkości, z różnymi statusami HTTP i opóźnieniami.
Będziemy zbierać logi z kontenerów dockera, które można znaleźć pod ścieżką /var/lib/docker/containers//-json.log
W pliku docker-compose.yml konfigurujemy Promtail i wskazujemy ścieżkę do konfigu:
promtail:
image: grafana/promtail:1.4.1
// ...
volumes:
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- promtail-data:/var/lib/promtail/positions
- ${PWD}/promtail/docker.yml:/etc/promtail/promtail.yml
command:
- '-config.file=/etc/promtail/promtail.yml'
// ...
Dodajemy w promtail.yml ścieżkę do logów (w konfiguracji jest opcja "docker", która robi to samo w jednej linii, ale byłoby to mniej czytelne):
scrape_configs:
- job_name: containers
static_configs:
labels:
job: containerlogs
__path__: /var/lib/docker/containers/*/*log # tylko dla linuxaPo włączeniu takiej konfiguracji logi ze wszystkich kontenerów będą trafiały do Loki. Aby tego uniknąć, zmieniamy ustawienia testowego nginx w docker-compose.yml — dodajemy pole logowania tag:
proxy:
image: nginx.test.v3
//…
logging:
driver: "json-file"
options:
tag: "{{.ImageName}}|{{.Name}}"Poprawiamy promtail.yml i konfigurujemy Pipeline. Na wejściu znajdują się logi w następującym formacie:
{"log":"u001b[0;33;1mnginx.1 | u001b[0mnginx.test 172.28.0.3 - - [13/Jun/2020:23:25:50 +0000] \"GET /api/index HTTP/1.1\" 200 0 \"-\" \"Stub_Bot/0.1\" \"0.096\"n","stream":"stdout","attrs":{"tag":"nginx.promtail.test|proxy.prober"},"time":"2020-06-13T23:25:50.66740443Z"}
{"log":"u001b[0;33;1mnginx.1 | u001b[0mnginx.test 172.28.0.3 - - [13/Jun/2020:23:25:50 +0000] \"GET /200 HTTP/1.1\" 200 0 \"-\" \"Stub_Bot/0.1\" \"0.000\"n","stream":"stdout","attrs":{"tag":"nginx.promtail.test|proxy.prober"},"time":"2020-06-13T23:25:50.702925272Z"}Etap Pipeline:
- json:
expressions:
stream: stream
attrs: attrs
tag: attrs.tagEkstrahujemy z przychodzącego JSON pola stream, attrs, attrs.tag (jeśli są) i umieszczamy je w mapie extracted.
- regex:
expression: ^(?P([^|]+))|(?P([^|]+))$
source: "tag"Jeśli udało się umieścić pole tag w mapie extracted, za pomocą wyrażenia regularnego ekstrahujemy nazwy obrazu i kontenera.
- labels:
image_name:
container_name:Przypisujemy etykiety. Jeśli w danych extracted znajdą się klucze image_name i container_name, ich wartości zostaną przypisane odpowiednim etykietom.
- match:
selector: '{job="docker",container_name="",image_name=""}'
action: dropOdrzucamy wszystkie logi, w których nie znaleziono ustawionych etykiet image_name i container_name.
- match:
selector: '{image_name="nginx.promtail.test"}'
stages:
- json:
expressions:
row: logDla wszystkich logów, w których image_name wynosi nginx.promtail.test, ekstrahujemy z oryginalnego logu pole log i umieszczamy je w mapie extracted z kluczem row.
- regex:
# suppress forego colors
expression: .+nginx.+|.+[0m(?P[a-z_.-]+) +(?P.+)
source: logrowOczyszczamy wejściowy ciąg za pomocą wyrażeń regularnych i wyciągamy virtual host nginx oraz ciąg logu nginx.
- regex:
source: nginxlog
expression: ^(?P[w.]+) - (?P[^ ]*) [(?P[^ ]+).*] "(?P[^ ]*) (?P[^ ]*) (?P[^ ]*)" (?P[d]+) (?P[d]+) "(?P[^"]*)" "(?P[^"]*)"( "(?P[d.]+)")?Parsujemy log nginx za pomocą wyrażeń regularnych.
- regex:
source: request_url
expression: ^.+.(?Pjpg|jpeg|gif|png|ico|css|zip|tgz|gz|rar|bz2|pdf|txt|tar|wav|bmp|rtf|js|flv|swf|html|htm)$
- regex:
source: request_url
expression: ^/photo/(?P[^/?.]+).*$
- regex:
source: request_url
expression: ^/api/(?P[^/?.]+).*$Analizujemy request_url. Za pomocą wyrażeń regularnych określamy przeznaczenie zapytania: do statyki, do zdjęć, do API i ustawiamy w mapie extracted odpowiedni klucz.
- template:
source: request_type
template: "{{if .photo}}photo{{else if .static_type}}static{{else if .api_request}}api{{else}}other{{end}}"Za pomocą operatorów warunkowych w szablonie sprawdzamy ustawione pola w mapie extracted i ustalamy dla pola request_type odpowiednie wartości: photo, static, API. Ustawiamy other, jeśli się nie udało. Teraz request_type zawiera typ zapytania.
- labels:
api_request:
virtual_host:
request_type:
status:Ustawiamy etykiety api_request, virtual_host, request_type oraz status (status HTTP) na podstawie tego, co udało się umieścić w mapie extracted.
- output:
source: nginx_log_rowZmiana output. Teraz w Loki wysyłany jest oczyszczony log nginx z mapy extracted.

Po uruchomieniu podanego konfigu można zobaczyć, że każdemu wpisowi przypisano etykiety na podstawie danych z logu.
Należy pamiętać, że wydobywanie etykiet z dużą liczbą wartości (kardynalność) może znacznie spowolnić działanie Loki. To znaczy, że nie warto umieszczać w indeksie, na przykład, user_id. Więcej na ten temat można przeczytać w artykule “”. Ale to nie znaczy, że nie można szukać po user_id bez indeksów. Należy użyć filtrów podczas wyszukiwania („grepować” w danych), a indeks w tym przypadku działa jako identyfikator strumienia.
Wizualizacja logów

Loki może pełnić rolę źródła danych dla wykresów Grafana, używając LogQL. Obsługiwane są następujące funkcje:
- rate — liczba wpisów na sekundę;
- count over time — liczba wpisów w określonym zakresie.
Dostępne są również funkcje agregacyjne Sum, Avg i inne. Można tworzyć dość skomplikowane wykresy, na przykład wykres liczby błędów HTTP:

Standardowe źródło danych Loki jest nieco ograniczone funkcjonalnie w porównaniu do źródła danych Prometheus (na przykład, nie można zmienić legendy), ale Loki można podłączyć jako źródło o typie Prometheus. Nie jestem pewien, czy jest to dokumentowane zachowanie, ale, sądząc po odpowiedzi deweloperów “”, na przykład, jest to całkowicie dozwolone, a Loki jest w pełni zgodny z PromQL.
Dodajemy Loki jako źródło danych o typie Prometheus i dopisujemy URL /loki:

I możemy tworzyć wykresy, tak jak w przypadku, gdybyśmy pracowali z metrykami z Prometheus:

Myślę, że rozbieżność w funkcjonalności jest tymczasowa i deweloperzy w przyszłości to naprawią.

Metryki
W Loki dostępna jest możliwość wydobywania metryk liczbowych z logów i wysyłania ich do Prometheus. Na przykład, w logu nginx znajduje się liczba bajtów w odpowiedzi, a także, przy pewnej modyfikacji standardowego formatu logów, czas w sekundach, który był potrzebny na odpowiedź. Te dane można wydobyć i wysłać do Prometheus.
Dodajemy jeszcze jedną sekcję do promtail.yml:
- match:
selector: '{request_type="api"}'
stages:
- metrics:
http_nginx_response_time:
type: Histogram
description: "czas odpowiedzi ms"
source: response_time
config:
buckets: [0.010,0.050,0.100,0.200,0.500,1.0]
- match:
selector: '{request_type=~"static|photo"}'
stages:
- metrics:
http_nginx_response_bytes_sum:
type: Counter
description: "suma bajtów odpowiedzi"
source: bytes_out
config:
action: add
http_nginx_response_bytes_count:
type: Counter
description: "liczba bajtów odpowiedzi"
source: bytes_out
config:
action: incOpcja pozwala na definiowanie i aktualizowanie metryk na podstawie danych z extracted map. Te metryki nie są przesyłane do Loki — pojawiają się w Promtail /metrics endpoint. Prometheus musi być skonfigurowany, aby otrzymywać dane uzyskane na tym etapie. W podanym przykładzie dla request_type="api" zbieramy metrykę-histogram. Z tym rodzajem metryk wygodnie jest uzyskać percentyle. Dla statyk i zdjęć zbieramy sumę bajtów oraz liczbę wierszy, w których otrzymaliśmy bajty, aby obliczyć wartość średnią.
Więcej informacji o metrykach przeczytasz .
Otwieramy port na Promtail:
promtail:
image: grafana/promtail:1.4.1
container_name: monitoring.promtail
expose:
- 9080
ports:
- "9080:9080"Upewniamy się, że metryki z prefiksem promtail_custom się pojawiły:

Konfigurujemy Prometheus. Dodajemy job promtail:
- job_name: 'promtail'
scrape_interval: 10s
static_configs:
- targets: ['promtail:9080']I rysujemy wykres:

W ten sposób możemy dowiedzieć się na przykład o czterech najwolniejszych zapytaniach. Można również ustawić monitorowanie dla tych danych metrycznych.
Skalowanie
Loki może działać w trybie pojedynczym (single binary mode) lub w trybie rozproszonym (horizontally-scalable mode). W tym drugim przypadku może przechowywać dane w chmurze, z chunkami i indeksami przechowywanymi osobno. W wersji 1.5 wprowadzono możliwość przechowywania w jednym miejscu, ale na razie nie jest ona zalecana do użycia w produkcji.

Chunki można przechowywać w magazynie zgodnym z S3, a do przechowywania indeksów można używać baz danych skalowalnych poziomo: Cassandra, BigTable lub DynamoDB. Inne części Loki — Distributory (do zapisu) i Querier (do zapytań) — są stateless i również skalują się poziomo.
Na konferencji DevOpsDays Vancouver 2019 jeden z uczestników Callum Styan ogłosił, że z Loki jego projekt ma petabajty logów z indeksem mniejszym niż 1% całkowitego rozmiaru: "”.
Porównanie Loki i ELK
Rozmiar indeksu
Do testowania uzyskanego rozmiaru indeksu wziąłem logi z kontenera nginx, dla którego ustawiano Pipeline opisany powyżej. Plik z logami zawierał 406 624 wiersze o łącznej objętości 109 MB. Logi generowano przez godzinę, około 100 zapisów na sekundę.
Przykład dwóch wierszy z logu:

Przy indeksowaniu ELK dało to rozmiar indeksu 30,3 MB:

W przypadku Loki dało to około 128 kB indeksu i około 3,8 MB danych w chunkach. Warto zauważyć, że log został sztucznie wygenerowany i nie różnił się dużą różnorodnością danych. Prosty gzip na początkowym dokerskim logu JSON z danymi dał kompresję 95,4%, a biorąc pod uwagę, że do samego Loki wysyłany był tylko oczyszczony log nginx, to kompresja do 4 MB jest zrozumiała. Całkowita liczba unikalnych wartości dla etykiet Loki wyniosła 35, co wyjaśnia mały rozmiar indeksu. Dla ELK log również był oczyszczany. W ten sposób, Loki skompresował dane pierwotne o 96%, a ELK - o 70%.
Zużycie pamięci

Porównując cały stos Prometheus i ELK, Loki „pożera” o kilka razy mniej. Oczywiście, usługa napisana w Go zużywa mniej, niż usługa napisana w Javie, a porównanie rozmiaru JVM Heap Elasticsearch i przydzielonej pamięci dla Loki jest niewłaściwe, jednak warto zauważyć, że Loki używa znacznie mniej pamięci. Jego przewaga w zakresie CPU nie jest tak oczywista, ale również występuje.
Szybkość
Loki szybciej „przetwarza” logi. Szybkość zależy od wielu czynników - jakie to logi, jak wyrafinowanie je parsujemy, sieć, dysk itd. - ale jest zdecydowanie wyższa niż w przypadku ELK (w moim teście - około dwa razy). Wyjaśnia to fakt, że Loki umieszcza znacznie mniej danych w indeksie i w związku z tym wydaje mniej czasu na indeksowanie. Sytuacja z szybkością wyszukiwania jest odwrotna: Loki zauważalnie spowalnia przy danych o wielkości przekraczającej kilka gigabajtów, podczas gdy ELK ma prędkość wyszukiwania niezależną od rozmiaru danych.
Wyszukiwanie w logach
Loki znacznie ustępuje ELK pod względem możliwości wyszukiwania w logach. Grep z wyrażeniami regularnymi to potężne narzędzie, ale ustępuje dojrzałej bazie danych. Brak zapytań typu range, agregacja tylko po etykietach, niemożność wyszukiwania bez etykiet - wszystkie te ograniczenia utrudniają nam znajdowanie interesujących informacji w Loki. Nie oznacza to, że za pomocą Loki nie można nic znaleźć, ale określa sposób pracy z logami, kiedy najpierw znajdujesz problem na wykresach Prometheus, a następnie po tych etykietach szukasz, co się wydarzyło w logach.
Interfejs
Po pierwsze, to ładne (przepraszam, nie mogłem się powstrzymać). Grafana ma przyjemny w wyglądzie interfejs, ale Kibana jest znacznie bardziej funkcjonalna.
Zalety i wady Loki
Z zalet można wymienić to, że Loki integruje się z Prometheusem, co oznacza, że metryki i alerty otrzymujemy bez zbędnych konfiguracji. Jest wygodny do zbierania logów i ich przechowywania z Kubernetes Pods, ponieważ dziedziczy po Prometheusie możliwość odkrywania usług i automatycznie dodaje etykiety.
Do wad należy słaba dokumentacja. Niektóre rzeczy, takie jak cechy i możliwości Promtail, odkryłem dopiero w trakcie przeglądania kodu, na szczęście open-source. Kolejnym minusem są ograniczone możliwości parsowania. Na przykład, Loki nie potrafi parsować logów wieloliniowych. Warto również dodać, że Loki to stosunkowo młoda technologia (wydanie 1.0 miało miejsce w listopadzie 2019 roku).
Podsumowanie
Loki to w 100% interesująca technologia, która nadaje się do małych i średnich projektów, umożliwiając rozwiązywanie wielu problemów związanych z agregacją logów, przeszukiwaniem logów, monitorowaniem i analizą logów.
Nie używamy Loki w Badoo, ponieważ mamy stos ELK, który nam odpowiada i przez lata zyskał różne niestandardowe rozwiązania. Kluczową kwestią dla nas jest przeszukiwanie logów. Mając prawie 100 GB logów dziennie, ważne jest, aby móc wszystko znaleźć i to jeszcze szybciej. Do budowy wykresów i monitorowania wykorzystujemy inne rozwiązania, które są dostosowane do naszych potrzeb i zintegrowane ze sobą. Stos Loki ma wyraźne zalety, ale nie zaoferuje nam więcej, niż już posiadamy, a jego korzyści z pewnością nie zrekompensują kosztów migracji.
I choć po badaniach stało się jasne, że nie możemy używać Loki, mamy nadzieję, że ten post pomoże Wam w podjęciu decyzji.
Repozytorium z kodem użytym w artykule znajduje się .
Źródło: habr.com
