
In Badoo monitoriamo costantemente le nuove tecnologie e valutiamo se sia il caso di utilizzarle nel nostro sistema. Vogliamo condividere con la comunità una di queste ricerche. Essa è dedicata a Loki — un sistema di aggregazione dei log.
Loki è una soluzione per l'archiviazione e la visualizzazione dei log; questo stack fornisce anche un sistema flessibile per la loro analisi e per inviare dati a Prometheus. A maggio è uscito un nuovo aggiornamento, che i creatori stanno promuovendo attivamente. Siamo interessati a scoprire le funzionalità di Loki, quali opportunità offre e in quale misura può fungere da alternativa all'ELK — lo stack che stiamo utilizzando attualmente.
Cos'è Loki
Grafana Loki è un insieme di componenti per un sistema completo di gestione dei log. A differenza di altri sistemi simili, Loki si basa sull'idea di indicizzare solo i metadati dei log — labels (proprio come in Prometheus), mentre i log stessi vengono compressi in chunk separati.
,
Prima di passare alla descrizione di ciò che si può fare con Loki, voglio chiarire cosa si intende per "idea di indicizzare solo i metadati". Confrontiamo l'approccio di Loki e l'approccio all'indicizzazione nelle soluzioni tradizionali, come Elasticsearch, utilizzando una riga di log di nginx come esempio:
172.19.0.4 - - [01/Giu/2020:12:05:03 +0000] "GET /purchase?user_id=75146478&item_id=34234 HTTP/1.1" 500 8102 "-" "Stub_Bot/3.0" "0.001"I sistemi tradizionali analizzano l'intera stringa, comprese le aree con un gran numero di valori unici come user_id e item_id, e memorizzano tutto in grandi indici. Il vantaggio di questo approccio è che è possibile eseguire query complesse rapidamente, poiché quasi tutti i dati si trovano nell'indice. Tuttavia, questo comporta il costo di un indice enorme, che si traduce in requisiti di memoria elevati. Di fatto, l'indice full-text dei log è comparabile in dimensione agli stessi log. Per cercare rapidamente attraverso di esso, l'indice deve essere caricato in memoria. E più log ci sono, più rapidamente l'indice cresce e maggiore è la memoria che consuma.
L'approccio di Loki richiede l'estrazione solo dei dati necessari da una stringa, il cui numero è relativamente limitato. In questo modo, otteniamo un indice ridotto e possiamo cercare i dati filtrandoli per tempo e per campi indicizzati, per poi scansionare il resto con espressioni regolari o per ricerca di sottostringa. Il processo può sembrare non molto veloce, ma Loki suddivide la query in più parti ed esegue queste operazioni in parallelo, elaborando una grande quantità di dati in breve tempo. Il numero di shard e di richieste parallele in essi può essere configurato; così, la quantità di dati che possono essere elaborati per unità di tempo dipende linearmente dalle risorse fornite.
Questo compromesso tra un grande indice veloce e un indice più piccolo con una ricerca completa parallela consente a Loki di controllare i costi del sistema. Può essere facilmente configurato e scalato in base alle necessità.
Il stack Loki è composto da tre componenti: Promtail, Loki e Grafana. Promtail raccoglie i log, li elabora e li invia a Loki. Loki li memorizza. Grafana permette invece di interrogare i dati da Loki e di visualizzarli. In effetti, Loki può essere utilizzato non solo per memorizzare i log e cercare in essi. L'intero stack offre ampie possibilità per l'elaborazione e l'analisi dei dati in arrivo, utilizzando il metodo Prometheus.
La descrizione del processo di installazione può essere trovata .
Ricerca nei log
La ricerca nei log può essere fatta attraverso l'interfaccia speciale di Grafana — Explorer. Per le interrogazioni, si utilizza il linguaggio LogQL, molto simile al PromQL, usato in Prometheus. In un certo senso, può essere considerato come un grep distribuito.
L'interfaccia di ricerca appare così:

L'interrogazione stessa è composta da due parti: selector e filter. Il selector è la ricerca nei metadati indicizzati (etichette) assegnati ai log, mentre il filter è una stringa di ricerca o una regex attraverso la quale vengono filtrati i record definiti dal selector. Nell'esempio fornito: Le parentesi graffe denotano il selector, tutto ciò che segue è il filtro.
{image_name="nginx.promtail.test"} |= "index"A causa del principio di funzionamento di Loki, non è possibile eseguire interrogazioni senza il selector, ma le etichette possono essere rese quanto più generali possibile.
Il selettore è una coppia chiave-valore racchiusa tra parentesi graffe. È possibile combinare i selettori e impostare diverse condizioni di ricerca utilizzando gli operatori =, != o le espressioni regolari:
{instance=~"kafka-[23]",name!="kafka-dev"}
// Troverà i log con l'etichetta instance, aventi valore kafka-2, kafka-3, ed escluderà dev Il filtro è un testo o un regex che filtrerà tutte le informazioni ottenute dal selettore.
È possibile ottenere grafici ad-hoc dai dati ricevuti in modalità metrics. Ad esempio, si può scoprire la frequenza di apparizione nei log di nginx delle voci contenenti la stringa index:

Una descrizione completa delle funzionalità è disponibile nella documentazione .
Parsing dei log
Ci sono diversi modi per raccogliere i log:
- Utilizzando Promtail, il componente standard del framework per la raccolta dei log.
- Direttamente dal container Docker tramite
- Utilizzare Fluentd o Fluent Bit, che possono inviare dati a Loki. A differenza di Promtail, possiedono parser già pronti per quasi ogni tipo di log e gestiscono anche i log multiline.
Di solito, si utilizza Promtail per il parsing. Esso esegue tre attività:
- Trova le fonti di dati.
- Aggiunge etichette ad esse.
- Invia i dati a Loki.
Attualmente Promtail può leggere i log da file locali e dal journal di systemd. Deve essere installato su ogni macchina da cui si raccolgono i log.
Esiste un'integrazione con Kubernetes: Promtail rileva automaticamente lo stato del cluster tramite l'API REST di Kubernetes e raccoglie i log da nodo, servizio o pod, aggiungendo immediatamente le etichette basate sui metadati di Kubernetes (nome del pod, nome file, ecc.).
È possibile anche aggiungere etichette basate sui dati dai log tramite un Pipeline. Il Pipeline di Promtail può consistere in quattro tipi di fasi. Maggiori dettagli - , qui segnalerò alcune particolarità.
- Fasi di Parsing. Questa fase riguarda RegEx e JSON. In questo passaggio estraiamo i dati dai log nella cosiddetta extracted map. I dati possono essere estratti da JSON semplicemente copiando i campi necessari nella extracted map, oppure tramite espressioni regolari (RegEx), dove nella extracted map si ‘mappano’ i named groups. L'extracted map è un archivio key-value, dove la chiave è il nome del campo e il valore è il suo contenuto dai log.
- Fasi di Trasformazione. Questo stadio ha due opzioni: transform, dove definiamo le regole di trasformazione, e source — la fonte dei dati per la trasformazione dall'extracted map. Se tale campo non è presente nell'extracted map, verrà creato. In questo modo è possibile creare etichette che non si basano sull'extracted map. In questo passaggio possiamo manipolare i dati nell'extracted map utilizzando strumenti piuttosto potenti. . Inoltre, è importante ricordare che l'extracted map viene completamente caricata durante il parsing, il che consente, ad esempio, di verificare un valore in essa: “{{if .tag}il valore del tag esiste{end}}”. Il template supporta condizioni, cicli e alcune funzioni stringa, come Replace e Trim.
- Action stages. In questo stadio, è possibile eseguire operazioni sui dati estratti:
- Creare un'etichetta dai dati estratti, che verrà indicizzata da Loki.
- Modificare o impostare il timestamp dell'evento dal log.
- Modificare i dati (testo del log) che verranno inviati a Loki.
- Creare metriche.
- Filtering stages. Stadio match, in cui puoi inviare a /dev/null i record che non ci servono o indirizzarli per ulteriori elaborazioni.
Mostrerò con un esempio di trattamento di normali log nginx, come è possibile analizzare i log utilizzando Promtail.
Per il test utilizzeremo un'immagine modificata di nginx come nginx-proxy jwilder/nginx-proxy:alpine e un piccolo demone che può interrogare se stesso tramite HTTP. Il demone ha diversi endpoint a cui può rispondere con dimensioni di risposta variabili, stati HTTP diversi e latenze differenti.
Raccoglieremo i log dai contenitori Docker, che si possono trovare nel percorso /var/lib/docker/containers//-json.log
In docker-compose.yml configuriamo Promtail e indichiamo il percorso del file di configurazione:
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'
// ...
Aggiungiamo a promtail.yml il percorso dei log (nel file di configurazione c'è un'opzione 'docker' che fa la stessa cosa in una riga, ma non sarebbe così chiaro):
scrape_configs:
- job_name: containers
static_configs:
labels:
job: containerlogs
__path__: /var/lib/docker/containers/*/*log # solo per linuxAbilitando questa configurazione, i log di tutti i contenitori verranno inviati a Loki. Per evitare ciò, modifichiamo le impostazioni del test nginx in docker-compose.yml — aggiungiamo la registrazione con il campo tag:
proxy:
image: nginx.test.v3
//…
logging:
driver: "json-file"
options:
tag: "{{.ImageName}}|{{.Name}}"Modifichiamo promtail.yml e configuriamo il Pipeline. I log in entrata sono del seguente tipo:
{"log":"u001b[0;33;1mnginx.1 | u001b[0mnginx.test 172.28.0.3 - - [13/Giu/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/Giu/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"}Fase del pipeline:
- json:
espressioni:
stream: stream
attrs: attrs
tag: attrs.tagEstraiamo dai JSON in ingresso i campi stream, attrs, attrs.tag (se presenti) e li inseriamo nella mappa estratta.
- regex:
espressione: ^(?P([^|]+))|(?P([^|]+))$
sorgente: "tag"Se il campo tag è stato correttamente inserito nella mappa estratta, utilizziamo l'expression per estrarre i nomi dell'immagine e del contenitore.
- labels:
image_name:
container_name:Assegniamo le etichette. Se nella data estratta vengono trovate le chiavi image_name e container_name, i loro valori saranno assegnati alle etichette corrispondenti.
- match:
selettore: '{job="docker",container_name="",image_name=""}'
azione: scartaScartiamo tutti i log che non hanno le etichette image_name e container_name impostate.
- match:
selettore: '{image_name="nginx.promtail.test"}'
fasi:
- json:
espressioni:
row: logPer tutti i log in cui image_name è uguale a nginx.promtail.test, estraiamo dal log originale il campo log e lo inseriamo nella mappa estratta con la chiave row.
- regex:
# sopprimi i colori forego
expression: .+nginx.+|.+[0m(?P[a-z_.-]+) +(?P.+)
source: logrowPulisci la stringa di ingresso con espressioni regolari ed estrai il virtual host nginx e la riga del log nginx.
- regex:
source: nginxlog
expression: ^(?P[w.]+) - (?P[^ ]*) [(?P[^ ]+).*] "(?P[^ ]*) (?P[^ ]*) (?P[^ ]*)" (?P[d]+) (?P[d]+) "(?P[^"]*)" "(?P[^"]*)"( "(?P[d.]+)")?Parser il log nginx con espressioni regolari.
- 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[^/?.]+).*$Analizziamo request_url. Utilizzando regex, determiniamo lo scopo della richiesta: verso la risorsa statica, verso le foto, verso l'API e impostiamo nella mappa estratta la chiave corrispondente.
- template:
source: request_type
template: "{{if .photo}}photo{{else if .static_type}}static{{else if .api_request}}api{{else}}other{{end}}"Utilizzando gli operatori condizionali in Template, controlliamo i campi impostati nella mappa estratta e impostiamo i valori richiesti per il campo request_type: photo, static, API. Assegniamo other se non ci riesce. Ora request_type contiene il tipo di richiesta.
- labels:
api_request:
virtual_host:
request_type:
status:Impostiamo le etichette api_request, virtual_host, request_type e status (HTTP status) in base a ciò che siamo riusciti a inserire nella mappa estratta.
- output:
source: nginx_log_rowModifichiamo l'output. Ora nel Loki viene inviato il log nginx pulito dalla mappa estratta.

Dopo aver avviato la configurazione fornita, è possibile vedere che a ciascun record sono state assegnate etichette in base ai dati del log.
È importante tenere presente che l'estrazione di etichette con un numero elevato di valori (cardinalità) può rallentare significativamente il funzionamento di Loki. Quindi non è consigliabile inserire nell'indice, ad esempio, user_id. Maggiori dettagli in "". Tuttavia, questo non significa che non si possa cercare per user_id senza indici. È necessario utilizzare filtri durante la ricerca ("greppare" nei dati), mentre l'indice funge da identificatore del flusso.
Visualizzazione dei log

Loki può fungere da fonte di dati per i grafici Grafana, utilizzando LogQL. Sono supportate le seguenti funzioni:
- rate — il numero di registrazioni al secondo;
- count over time — il numero di registrazioni in un intervallo specificato.
Sono inoltre disponibili funzioni di aggregazione come Sum, Avg e altre. È possibile creare grafici piuttosto complessi, ad esempio un grafico sul numero di errori HTTP:

La fonte dati standard Loki ha funzionalità ridotte rispetto alla fonte dati Prometheus (ad esempio, non è possibile modificare la legenda), ma Loki può essere connesso come fonte di tipo Prometheus. Non sono sicuro che questo comportamento sia documentato, ma, basandomi sulla risposta degli sviluppatori “”, ad esempio, ciò è perfettamente legittimo e Loki è completamente compatibile con PromQL.
Aggiungiamo Loki come fonte dati di tipo Prometheus e aggiungiamo l'URL /loki:

E possiamo creare grafici, proprio come se stessimo lavorando con le metriche di Prometheus:

Penso che la disuguaglianza nelle funzionalità sia temporanea e gli sviluppatori la correggeranno in futuro.

Metriche
In Loki è possibile estrarre metriche numeriche dai log e inviarle a Prometheus. Ad esempio, nei log di nginx è presente il numero di byte per risposta e, con una modifica specifica del formato standard del log, anche il tempo in secondi impiegato per la risposta. Questi dati possono essere estratti e inviati a Prometheus.
Aggiungiamo un'altra sezione a promtail.yml:
- match:
selector: '{request_type="api"}'
stages:
- metrics:
http_nginx_response_time:
type: Histogram
description: "tempo di risposta 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: "somma dei byte della risposta"
source: bytes_out
config:
action: add
http_nginx_response_bytes_count:
type: Counter
description: "conteggio dei byte della risposta"
source: bytes_out
config:
action: incL'opzione consente di definire e aggiornare le metriche basate sui dati estratti dalla mappa. Queste metriche non vengono inviate a Loki, ma appaiono nell'endpoint Promtail /metrics. Prometheus deve essere configurato per ricevere i dati ottenuti in questa fase. Nell'esempio fornito, per request_type="api", raccogliamo una metrica a istogramma. Con questo tipo di metriche è facile ottenere percentili. Per la statica e le foto, raccogliamo la somma dei byte e il numero di righe in cui abbiamo ottenuto byte, per calcolare il valore medio.
Leggi di più sulle metriche .
Apriamo la porta su Promtail:
promtail:
image: grafana/promtail:1.4.1
container_name: monitoring.promtail
expose:
- 9080
ports:
- "9080:9080"Assicuriamoci che le metriche con il prefisso promtail_custom siano apparse:

Configuriamo Prometheus. Aggiungiamo il job promtail:
- job_name: 'promtail'
scrape_interval: 10s
static_configs:
- targets: ['promtail:9080']E tracciamo il grafico:

In questo modo, ad esempio, è possibile scoprire le quattro richieste più lente. È anche possibile impostare il monitoraggio su queste metriche.
Scalabilità
Loki può operare sia in modalità singola (single binary mode) che in modalità shardabile (horizontally-scalable mode). Nel secondo caso, può archiviare i dati nel cloud, con i chunk e l'indice memorizzati separatamente. Nella versione 1.5 è stata implementata la possibilità di archiviare tutto in un'unica posizione, ma attualmente non è consigliato utilizzarla in produzione.

I chunk possono essere memorizzati in uno storage compatibile con S3, mentre per gli indici è consigliabile utilizzare database orizzontalmente scalabili come Cassandra, BigTable o DynamoDB. Altre parti di Loki — Distributors (per la scrittura) e Querier (per le query) — sono senza stato e possono essere scalati orizzontalmente.
Alla conferenza DevOpsDays Vancouver 2019, uno dei partecipanti, Callum Styan, ha dichiarato che con Loki il suo progetto gestisce petabyte di log con un indice inferiore all'1% delle dimensioni totali: “”.
Confronto tra Loki e ELK
Dimensione dell'indice
Per testare la dimensione dell'indice ottenuta, ho utilizzato i log da un container nginx, per il quale era stato configurato il Pipeline descritto sopra. Il file di log conteneva 406.624 righe per un volume totale di 109 MB. I log sono stati generati per un'ora, circa 100 registrazioni al secondo.
Esempio di due righe dal log:

Durante l'indicizzazione con ELK, questo ha prodotto una dimensione dell'indice di 30,3 MB:

Nel caso di Loki, questo ha fornito circa 128 Kb di indice e circa 3,8 Mb di dati in chunk. È importante notare che il log è stato generato artificialmente e non presentava una grande varietà di dati. Un semplice gzip sul log JSON originale di Docker con i dati ha fornito una compressione del 95,4%, e considerando che nel Loki veniva inviato solo il log nginx ripulito, è comprensibile la compressione a 4 Mb. Il numero totale di valori unici per le etichette di Loki era 35, il che spiega la piccola dimensione dell'indice. Anche per ELK, il log è stato ripulito. In questo modo, Loki ha compresso i dati originali del 96%, mentre ELK del 70%.
Consumo di memoria

Confrontando l'intero stack Prometheus ed ELK, Loki consuma significativamente meno. È chiaro che un servizio in Go consuma meno rispetto a uno in Java, e il confronto tra la dimensione dell'heap JVM di Elasticsearch e la memoria allocata per Loki non è corretto, ma si deve comunque notare che Loki utilizza molta meno memoria. Il suo vantaggio in termini di CPU non è così evidente, ma è comunque presente.
Velocità
Loki è più rapido nel "catturare" i log. La velocità dipende da molti fattori: che tipo di log sono, quanto complessamente li analizziamo, la rete, il disco, ecc. Tuttavia, è sicuramente superiore rispetto a ELK (nel mio test, circa il doppio). Questo è spiegato dal fatto che Loki inserisce molte meno informazioni nell'indice, e di conseguenza, impiega meno tempo per indicizzare. Tuttavia, la situazione è inversa per quanto riguarda la velocità di ricerca: Loki rallenta sensibilmente con dati superiori a pochi gigabyte, mentre per ELK la velocità di ricerca non dipende dalle dimensioni dei dati.
Ricerca nei log
Loki è significativamente inferiore a ELK per quanto riguarda le capacità di ricerca nei log. Grep con espressioni regolari è uno strumento potente, ma non si può paragonare a un database maturo. La mancanza di query per range, l'aggregazione solo per etichette e l'impossibilità di cercare senza etichette limitano la nostra possibilità di trovare informazioni rilevanti in Loki. Ciò non significa che non si possa trovare nulla con Loki, ma definisce il flusso di lavoro con i log, dove prima si identifica un problema attraverso i grafici di Prometheus e poi, utilizzando queste etichette, si cerca cosa sia accaduto nei log.
Interfaccia
In primo luogo, è bello (scusate, non ho potuto resistere). Grafana ha un'interfaccia gradevole, ma Kibana è molto più funzionale.
Pro e contro di Loki
Tra i punti a favore, si può notare che Loki si integra con Prometheus, quindi otteniamo metriche e allerta direttamente. È comodo per raccogliere e archiviare i log insieme ai Pods di Kubernetes, poiché eredita dal service discovery di Prometheus e aggiunge automaticamente le etichette.
Tra i punti a sfavore, la documentazione è debole. Alcune cose, come le peculiarità e le funzionalità di Promtail, le ho scoperte solo studiando il codice, per fortuna open-source. Un altro difetto sono le limitate capacità di parsing. Ad esempio, Loki non può analizzare log multilinea. Inoltre, tra i difetti c'è il fatto che Loki è una tecnologia relativamente nuova (il rilascio 1.0 è avvenuto a novembre 2019).
Conclusione
Loki è una tecnologia al 100% interessante, adatta per progetti piccoli e medi, permettendo di affrontare molteplici compiti di aggregazione dei log, ricerca tra i log, monitoraggio e analisi dei log.
Non utilizziamo Loki in Badoo, poiché abbiamo un stack ELK che ci soddisfa e nel quale, nel corso degli anni, abbiamo sviluppato diverse soluzioni personalizzate. Per noi, la sfida principale è la ricerca nei log. Con quasi 100 GB di log al giorno, è fondamentale poter trovare tutto e anche di più, e farlo rapidamente. Per la creazione di grafici e il monitoraggio utilizziamo altre soluzioni, progettate per le nostre esigenze e integrate tra loro. L'stack Loki ha vantaggi significativi, ma non ci fornirà di più rispetto a ciò che già abbiamo, e i suoi benefici non giustificherebbero il costo della migrazione.
E anche se dopo la ricerca è diventato chiaro che non possiamo utilizzare Loki, speriamo che questo post possa aiutarvi nella scelta.
Il repository con il codice utilizzato nell'articolo si trova .
Fonte: habr.com
