No Badoo, monitoramos constantemente novas tecnologias e avaliamos se vale a pena usá-las em nosso sistema. Gostaríamos de compartilhar um desses estudos com a comunidade. É dedicado ao Loki, um sistema de agregação de logs.
Loki é uma solução para armazenar e visualizar logs, e essa pilha também fornece um sistema flexível para analisá-los e enviar dados ao Prometheus. Em maio, foi lançada outra atualização, promovida ativamente pelos criadores. Estávamos interessados no que o Loki pode fazer, quais recursos ele oferece e até que ponto ele pode atuar como uma alternativa ao ELK, a pilha que usamos agora.
O que é Loki
Grafana Loki é um conjunto de componentes para um sistema completo de trabalho com logs. Ao contrário de outros sistemas semelhantes, o Loki é baseado na ideia de indexar apenas metadados de log - rótulos (os mesmos do Prometheus) e compactar os próprios logs em pedaços separados.
Antes de entrarmos no que você pode fazer com o Loki, quero esclarecer o que queremos dizer com “a ideia de indexar apenas metadados”. Vamos comparar a abordagem Loki e a abordagem de indexação em soluções tradicionais como Elasticsearch, usando o exemplo de uma linha do log nginx:
Os sistemas tradicionais analisam a linha inteira, incluindo campos com um grande número de valores exclusivos de user_id e item_id, e armazenam tudo em grandes índices. A vantagem dessa abordagem é que você pode executar consultas complexas rapidamente, já que quase todos os dados estão no índice. Mas isso tem um custo, pois o índice se torna grande, o que se traduz em requisitos de memória. Como resultado, o índice de log de texto completo é comparável em tamanho aos próprios logs. Para pesquisá-lo rapidamente, o índice deve ser carregado na memória. E quanto mais logs, mais rápido o índice cresce e mais memória ele consome.
A abordagem Loki exige que apenas os dados necessários sejam extraídos de uma string, cujo número de valores é pequeno. Dessa forma, obtemos um pequeno índice e podemos pesquisar os dados filtrando-os por tempo e por campos indexados e, em seguida, verificando o restante com expressões regulares ou pesquisa de substring. O processo não parece dos mais rápidos, mas Loki divide a solicitação em diversas partes e as executa em paralelo, processando uma grande quantidade de dados em pouco tempo. O número de fragmentos e solicitações paralelas neles é configurável; assim, a quantidade de dados que pode ser processada por unidade de tempo depende linearmente da quantidade de recursos fornecidos.
Essa compensação entre um índice grande e rápido e um índice de força bruta pequeno e paralelo permite que Loki controle o custo do sistema. Pode ser configurado e expandido de forma flexível de acordo com as necessidades.
A pilha Loki consiste em três componentes: Promtail, Loki, Grafana. Promtail coleta logs, processa-os e envia-os para Loki. Loki os mantém. E o Grafana pode solicitar dados do Loki e exibi-los. Em geral, o Loki pode ser usado não apenas para armazenar logs e pesquisá-los. A pilha inteira oferece grandes oportunidades para processar e analisar dados recebidos usando o método Prometheus.
Uma descrição do processo de instalação pode ser encontrada aqui.
Pesquisar por registros
Você pode pesquisar os logs em uma interface especial do Grafana - Explorer. As consultas utilizam a linguagem LogQL, que é muito semelhante ao PromQL utilizado no Prometheus. Em princípio, pode ser pensado como um grep distribuído.
A interface de pesquisa é semelhante a esta:
A solicitação em si consiste em duas partes: seletor e filtro. Seletor é uma pesquisa que usa metadados indexados (rótulos) atribuídos a logs, e filtro é uma string de pesquisa ou regexp que filtra os registros definidos pelo seletor. No exemplo dado: Entre chaves existe um seletor, tudo depois é um filtro.
{image_name="nginx.promtail.test"} |= "index"
Devido à forma como o Loki funciona, você não pode fazer consultas sem um seletor, mas os rótulos podem ser tão gerais quanto você desejar.
Um seletor é um valor-chave entre chaves. Você pode combinar seletores e especificar diferentes condições de pesquisa usando os operadores =, != ou expressões regulares:
{instance=~"kafka-[23]",name!="kafka-dev"}
// Найдёт логи с лейблом instance, имеющие значение kafka-2, kafka-3, и исключит dev
Um filtro é um texto ou regexp que filtrará todos os dados recebidos pelo seletor.
É possível obter gráficos ad-hoc com base nos dados recebidos no modo métrico. Por exemplo, você pode descobrir com que frequência uma entrada contendo o índice de string aparece nos logs do nginx:
Uma descrição completa dos recursos pode ser encontrada na documentação LogQL.
Análise de log
Existem várias maneiras de coletar logs:
Usando Promtail, um componente padrão da pilha para coleta de logs.
Use Fluentd ou Fluent Bit, que pode enviar dados para Loki. Ao contrário do Promtail, eles possuem analisadores prontos para quase qualquer tipo de log e também podem lidar com logs multilinhas.
Normalmente o Promtail é usado para análise. Ele faz três coisas:
Encontra fontes de dados.
Anexa etiquetas a eles.
Envia dados para Loki.
Atualmente o Promtail pode ler logs de arquivos locais e do diário do systemd. Ele deve ser instalado em cada máquina da qual os logs são coletados.
Há integração com Kubernetes: o Promtail automaticamente, por meio da API REST do Kubernetes, reconhece o estado do cluster e coleta logs de um nó, serviço ou pod, colocando imediatamente rótulos com base nos metadados do Kubernetes (nome do pod, nome do arquivo, etc.) .
Você também pode pendurar rótulos com base nos dados do log usando Pipeline. O Pipeline Promtail pode consistir em quatro tipos de estágios. Mais detalhes em documentação oficial, notarei imediatamente algumas nuances.
Estágios de análise. Este é o estágio RegEx e JSON. Nesta fase, extraímos os dados dos logs para o chamado mapa extraído. Podemos extrair do JSON simplesmente copiando os campos necessários no mapa extraído ou por meio de expressões regulares (RegEx), onde grupos nomeados são “mapeados” no mapa extraído. O mapa extraído é um armazenamento de valor-chave, onde chave é o nome do campo e valor é o valor dos logs.
Estágios de transformação. Esta etapa possui duas opções: transform, onde definimos as regras de transformação, e source - a fonte de dados para transformação do mapa extraído. Caso não exista tal campo no mapa extraído, ele será criado. Desta forma é possível criar rótulos que não sejam baseados no mapa extraído. Nesta fase podemos manipular os dados no mapa extraído usando uma ferramenta bastante poderosa Modelo Golang. Além disso, devemos lembrar que o mapa extraído é carregado integralmente durante a análise, o que possibilita, por exemplo, verificar o valor nele contido: “{{if .tag}o valor da tag existe{end}}”. O modelo oferece suporte a condições, loops e algumas funções de string, como Substituir e Aparar.
Etapas de ação. Neste ponto você pode fazer algo com o conteúdo extraído:
Crie um rótulo a partir dos dados extraídos, que será indexado pelo Loki.
Altere ou defina a hora do evento no log.
Altere os dados (texto do log) que irão para o Loki.
Crie métricas.
Estágios de filtragem. O estágio de correspondência, onde podemos enviar entradas que não precisamos para /dev/null ou encaminhá-las para processamento posterior.
Usando um exemplo de processamento de logs nginx regulares, mostrarei como você pode analisar logs usando o Promtail.
Para o teste, vamos tomar como nginx-proxy uma imagem nginx modificada jwilder/nginx-proxy:alpine e um pequeno daemon que pode se perguntar via HTTP. O daemon possui vários endpoints, aos quais pode fornecer respostas de diferentes tamanhos, com diferentes status HTTP e com diferentes atrasos.
Coletaremos logs de contêineres docker, que podem ser encontrados no caminho /var/lib/docker/containers/ / -json.log
Em docker-compose.yml configuramos o Promtail e especificamos o caminho para a configuração:
Adicione o caminho dos logs para promtail.yml (há uma opção “docker” na configuração, que faz a mesma coisa em uma linha, mas não ficaria tão claro):
scrape_configs:
- job_name: containers
static_configs:
labels:
job: containerlogs
__path__: /var/lib/docker/containers/*/*log # for linux only
Quando esta configuração estiver habilitada, os logs de todos os contêineres serão enviados ao Loki. Para evitar isso, alteramos as configurações do nginx de teste em docker-compose.yml - adicionamos um campo de tag de registro:
Se conseguimos colocar o campo tag no mapa extraído, então usando regexp extraímos os nomes da imagem e do container.
- labels:
image_name:
container_name:
Atribuímos rótulos. Se as chaves image_name e container_name forem encontradas nos dados extraídos, seus valores serão atribuídos aos rótulos correspondentes.
- match:
selector: '{job="docker",container_name="",image_name=""}'
action: drop
Descartamos todos os logs que não possuem os rótulos image_name e container_name instalados.
Vamos analisar request_url. Usando regexp determinamos a finalidade da solicitação: para dados estáticos, para fotos, para API e definimos a chave correspondente no mapa extraído.
- template:
source: request_type
template: "{{if .photo}}photo{{else if .static_type}}static{{else if .api_request}}api{{else}}other{{end}}"
Usando operadores condicionais no Template, verificamos os campos instalados no mapa extraído e definimos os valores necessários para o campo request_type: foto, estático, API. Atribua outro se falhar. request_type agora contém o tipo de solicitação.
Definimos os rótulos api_request, virtual_host, request_type e status (status HTTP) com base no que conseguimos colocar no mapa extraído.
- output:
source: nginx_log_row
Alterar saída. Agora, o log nginx limpo do mapa extraído vai para Loki.
Depois de executar a configuração acima, você pode ver que cada entrada recebe rótulos com base nos dados do log.
Uma coisa a ter em mente é que recuperar rótulos com um grande número de valores (cardinalidade) pode desacelerar significativamente o Loki. Ou seja, você não deve colocar, por exemplo, user_id no índice. Leia mais sobre isso no artigo “Como os rótulos no Loki podem tornar as consultas de log mais rápidas e fáceis" Mas isso não significa que você não possa pesquisar por user_id sem índices. Você precisa usar filtros ao pesquisar (“capturar” os dados), e o índice aqui atua como um identificador de fluxo.
Visualização de logs
Loki pode atuar como fonte de dados para gráficos Grafana usando LogQL. Os seguintes recursos são suportados:
taxa — número de registros por segundo;
contagem ao longo do tempo — o número de registros no intervalo especificado.
Existem também funções de agregação Sum, Avg e outras. Você pode construir gráficos bastante complexos, por exemplo, um gráfico do número de erros HTTP:
A fonte de dados padrão Loki tem funcionalidade um pouco reduzida em comparação com a fonte de dados Prometheus (por exemplo, você não pode alterar a legenda), mas Loki pode ser conectado como uma fonte com o tipo Prometheus. Não tenho certeza se este é um comportamento documentado, mas a julgar pela resposta dos desenvolvedores “Como configurar o Loki como fonte de dados do Prometheus? · Edição #1222 · grafana/loki”, por exemplo, é totalmente legal, e Loki é totalmente compatível com PromQL.
Adicione Loki como fonte de dados com o tipo Prometheus e adicione URL /loki:
E podemos fazer gráficos, como se estivéssemos trabalhando com métricas do Prometheus:
Acho que a discrepância na funcionalidade é temporária e os desenvolvedores corrigirão isso no futuro.
Métricas
Loki oferece a capacidade de extrair métricas numéricas de logs e enviá-las ao Prometheus. Por exemplo, o log nginx contém o número de bytes por resposta, bem como, com uma certa modificação no formato de log padrão, o tempo em segundos que levou para responder. Esses dados podem ser extraídos e enviados ao Prometheus.
A opção permite definir e atualizar métricas com base nos dados do mapa extraído. Essas métricas não são enviadas ao Loki - elas aparecem no endpoint Promtail /metrics. O Prometheus deve estar configurado para receber os dados recebidos nesta fase. No exemplo acima, para request_type=“api” coletamos uma métrica de histograma. Com este tipo de métrica é conveniente obter percentis. Para estática e foto, coletamos a soma dos bytes e o número de linhas em que recebemos bytes para calcular a média.
Desta forma você pode descobrir, por exemplo, as quatro consultas mais lentas. Você também pode configurar o monitoramento para essas métricas.
Escala
Loki pode estar no modo binário único ou no modo fragmentado (modo escalonável horizontalmente). No segundo caso, ele pode salvar os dados na nuvem, e os pedaços e o índice são armazenados separadamente. A versão 1.5 introduz a capacidade de armazenamento em um só lugar, mas ainda não é recomendado usá-lo em produção.
Os pedaços podem ser armazenados em armazenamento compatível com S3 e bancos de dados escaláveis horizontalmente podem ser usados para armazenar índices: Cassandra, BigTable ou DynamoDB. Outras partes do Loki - Distribuidores (para escrita) e Querier (para consultas) - não têm estado e também são dimensionadas horizontalmente.
Para testar o tamanho do índice resultante, peguei logs do contêiner nginx para o qual o Pipeline acima foi configurado. O arquivo de log continha 406 linhas com volume total de 624 MB. Os logs foram gerados em uma hora, aproximadamente 109 entradas por segundo.
Exemplo de duas linhas do log:
Quando indexado pelo ELK, obteve um tamanho de índice de 30,3 MB:
No caso do Loki, isso resultou em aproximadamente 128 KB de índice e aproximadamente 3,8 MB de dados em blocos. Vale ressaltar que o log foi gerado artificialmente e não possuía grande variedade de dados. Um simples gzip no log JSON do Docker original com dados deu uma compactação de 95,4%, e levando em consideração o fato de que apenas o log nginx limpo foi enviado para o próprio Loki, a compactação de até 4 MB é compreensível. O número total de valores exclusivos para rótulos Loki foi 35, o que explica o pequeno tamanho do índice. Para ELK, o log também foi apagado. Assim, Loki comprimiu os dados originais em 96% e ELK em 70%.
Consumo de memória
Se compararmos toda a pilha do Prometheus e do ELK, Loki “come” várias vezes menos. É claro que um serviço Go consome menos que um serviço Java, e comparar o tamanho do JVM Heap Elasticsearch e a memória alocada para Loki está incorreto, mas mesmo assim é importante notar que Loki usa muito menos memória. Sua vantagem de CPU não é tão óbvia, mas também está presente.
velocidade
Loki “devora” registros mais rápido. A velocidade depende de muitos fatores - que tipo de log são, quão sofisticados somos em analisá-los, rede, disco, etc. - mas é definitivamente maior que o ELK (no meu teste - cerca de duas vezes mais). Isso se explica pelo fato de Loki colocar muito menos dados no índice e, consequentemente, gastar menos tempo na indexação. Com a velocidade de pesquisa, a situação é oposta: Loki fica visivelmente mais lento em dados maiores que vários gigabytes, enquanto a velocidade de pesquisa do ELK não depende do tamanho dos dados.
Pesquisar por registros
Loki é significativamente inferior ao ELK em termos de recursos de pesquisa de log. Grep com expressões regulares é poderoso, mas é inferior a um banco de dados maduro. A falta de consultas de intervalo, agregação apenas por rótulos, a impossibilidade de pesquisar sem rótulos - tudo isso nos limita na busca por informações de interesse em Loki. Isso não significa que nada pode ser encontrado usando o Loki, mas define o fluxo de trabalho com logs quando você encontra pela primeira vez um problema nos gráficos do Prometheus e, em seguida, usa esses rótulos para procurar o que aconteceu nos logs.
Interface.
Em primeiro lugar, é lindo (desculpe, não resisti). O Grafana tem uma interface bonita, mas o Kibana é muito mais rico em recursos.
Prós e contras de Loki
Uma das vantagens é que o Loki se integra ao Prometheus, então obtemos métricas e alertas prontos para uso. É conveniente coletar logs e armazená-los em pods do Kubernetes, pois possui descoberta de serviço herdada do Prometheus e anexa rótulos automaticamente.
A desvantagem é a documentação fraca. Algumas coisas, por exemplo, os recursos e capacidades do Promtail, descobri apenas no processo de estudo do código, felizmente é de código aberto. Outra desvantagem são os fracos recursos de análise. Por exemplo, Loki não pode analisar logs multilinhas. Outra desvantagem é que Loki é uma tecnologia relativamente jovem (a versão 1.0 foi em novembro de 2019).
Conclusão
Loki é uma tecnologia 100% interessante e adequada para projetos de pequeno e médio porte, permitindo resolver diversos problemas de agregação de logs, busca de logs, monitoramento e análise de logs.
Não usamos Loki no Badoo porque temos uma pilha ELK adequada para nós e que foi repleta de várias soluções personalizadas ao longo dos anos. Para nós, o obstáculo é pesquisar nos registros. Com quase 100 GB de logs por dia, é importante para nós conseguirmos encontrar tudo e um pouco mais e fazê-lo rapidamente. Para mapeamento e monitoramento, utilizamos outras soluções adaptadas às nossas necessidades e integradas entre si. A pilha Loki tem benefícios tangíveis, mas não nos dará mais do que já temos, e os seus benefícios certamente não compensarão o custo da migração.
E embora depois de pesquisas tenha ficado claro que não podemos usar o Loki, esperamos que este post o ajude na sua escolha.
O repositório com o código utilizado no artigo está localizado aqui.