A KubeCon + CloudNativeCon di Seattle, l’11 dicembre 2018, Grafana Labs ha annunciato Loki: un aggregatore di log in stato alpha che indicizza solo un insieme di etichette per flusso, e non il testo delle righe. Il progetto è scritto in Go, ha licenza Apache 2.0, e il codice sta su github.com/grafana/loki.

Contesto

Nel 2018 lo stack di logging più diffuso gira intorno a Elasticsearch: un agente (Filebeat, Fluentd) spedisce le righe, Logstash o una pipeline equivalente le elabora, ed Elasticsearch costruisce un indice invertito su ogni termine. L’indice full-text rende possibili ricerche arbitrarie sul testo, ma si paga: ogni token diventa una voce nell’indice, e l’indice vuole disco veloce e memoria. Sui volumi di log applicativi di un cluster Kubernetes il rapporto fra dimensione dell’indice e dimensione dei dati grezzi tende a crescere, e con esso la fame di RAM e di SSD.

Tom Wilkie e David Kaltschmidt, che a marzo 2018 hanno scritto il primo documento di progettazione, partono da un’osservazione operativa: in molti flussi di lavoro nessuno cerca una parola qualsiasi in tutti i log del cluster. Si parte da un servizio noto — un namespace, un pod, un’applicazione — e dentro quel sottoinsieme si filtra con qualcosa di simile a grep. Se i log si consultano già così, indicizzare ogni token è lavoro pagato e mai usato.

Architettura

Loki riusa l’impianto di Cortex, l’implementazione di Prometheus scalabile in orizzontale a cui Wilkie ha lavorato. Le righe arrivano via push — l’opposto del modello pull di Prometheus — e passano per tre componenti.

Il distributor riceve le richieste HTTP con i flussi e le righe. Da ogni flusso calcola un hash a partire dalle sue etichette e, attraverso un anello di hashing consistente, trova l’ingester di destinazione. Con un fattore di replica maggiore di uno, lo stesso flusso finisce su più ingester.

L’ingester raccoglie in memoria le righe di un flusso dentro un chunk, lo comprime, e ogni tanto lo scarica sullo storage di lungo periodo. La compressione rende bene perché le righe di uno stesso flusso si somigliano.

Il querier esegue le interrogazioni. Una query ha due parti: un selettore di etichette — la stessa sintassi dei matcher di Prometheus, per esempio {app="webapp", env="prod"} — e un eventuale filtro di testo applicato alle righe del flusso selezionato. Il querier chiede agli ingester i dati ancora in memoria e allo storage i chunk già scaricati.

Lo storage è diviso in due. Un indice tiene la corrispondenza fra etichette e chunk; nelle prime versioni vive su un archivio NoSQL — DynamoDB, Google Bigtable o Cassandra. I chunk compressi, cioè il contenuto vero dei log, stanno su object storage: S3, Google Cloud Storage o un equivalente compatibile. L’indice resta piccolo perché contiene solo etichette, e fra i livelli citati l’object storage è il più economico.

Lato raccolta, l’agente di riferimento è Promtail: scopre i target — file, journald, pod Kubernetes — ricava le etichette dai metadati (per i pod, le label già presenti) e spedisce le righe al distributor. Le etichette dei log escono dalle stesse label che Prometheus usa per le metriche, e così dentro Grafana le due sorgenti si confrontano direttamente.

Il punto critico

Il compromesso è dichiarato, e va valutato prima di adottare Loki: senza indice full-text, un filtro di testo è una scansione lineare delle righe del flusso selezionato, non una lookup su indice invertito. Resta economico finché il selettore di etichette restringe abbastanza le righe da scorrere. Una query {namespace="x"} con un filtro su una stringa rara è veloce se namespace="x" isola pochi flussi; diventa cara se le etichette pescano una fetta larga del cluster e il filtro deve scorrere gigabyte di righe.

Da qui un vincolo operativo di progettazione: la cardinalità delle etichette va tenuta bassa. Promuovere a etichetta un campo ad alta cardinalità — l’identificativo di una richiesta, l’IP di un client — genera una marea di flussi distinti, ognuno col suo chunk; lo stesso errore di modellazione che in Prometheus fa esplodere il numero di serie temporali. Le etichette servono a spezzare i log in pochi insiemi stabili, non a rimpiazzare la ricerca sul contenuto.

Implicazioni

Il modello sposta il costo dallo storage al momento della query. Elasticsearch paga in anticipo — indice grande, hardware veloce — per rendere rapida ogni ricerca. Loki paga poco in scrittura e archiviazione, e paga la singola query in scansione, tanto più quanto peggio il selettore di etichette ha ristretto il campo. Per un team che arriva ai log partendo sempre dal servizio e poi filtra, il conto torna a favore di Loki; per chi cerca a tappeto sul testo di tutti i log, no.

La continuità con Prometheus è la parte più concreta sul piano operativo. Le stesse etichette descrivono metriche e log; in Grafana si passa da un grafico di metriche alle righe di log dello stesso servizio, sullo stesso intervallo, senza riconciliare due sistemi di nomi diversi. Chi già gestisce Prometheus si ritrova quasi lo stesso modello mentale.

Limiti

A gennaio 2019 Loki è dichiarato alpha dagli stessi autori: formato di storage, sintassi delle query e interfacce possono cambiare, e non è roba da mettere in produzione su log critici senza riserve. Il linguaggio di query è ancora elementare — selettore di etichette più filtro di testo — e privo delle aggregazioni che un PromQL offre sulle metriche. Manca una storia consolidata di esercizio su larga scala: i numeri sul costo rispetto a Elasticsearch vanno misurati sul proprio profilo di etichette e di query, non dati per scontati. E il vincolo di cardinalità non è un dettaglio di tuning ma un requisito: un modello di etichette sbagliato annulla il vantaggio di costo che è la ragione stessa del progetto.


Immagine di copertina: Screenshot di un dashboard di Grafana del 2018 con grafici di metriche su fondo scuro: pannelli a linee, indicatori e serie temporali — schermata di Joel Kennedy, pubblico dominio — https://commons.wikimedia.org/wiki/File:Grafana_screenshot_(2018).png