Loki系统研究及实践

2022-10-22,,,

背景

Loki 是 Grafana Labs 团队最新的开源项目,是一个水平可扩展,高可用性,多租户的日志聚合系统。它的设计非常经济高效且易于操作,因为它不会为日志内容编制索引,而是为每个日志流编制一组标签。项目受 Prometheus 启发。

Loki原理介绍

简介

项目地址:https://github.com/grafana/loki
loki官方文档:https://grafana.com/docs/loki/latest/

与其他日志聚合系统相比,Loki :

  • 不对日志进行全文索引。通过存储压缩非结构化日志和仅索引元数据,Loki 操作起来会更简单,更省成本。
  • 通过使用与 Prometheus 相同的标签记录流对日志进行索引和分组,这使得日志的扩展和操作效率更高。
  • 特别适合储存 Kubernetes Pod 日志; 诸如 Pod 标签之类的元数据会被自动删除和编入索引。
  • 受 Grafana 原生支持。

Loki 由3部分组成:

  • loki是主服务器,负责存储日志和处理查询。
  • promtail 是代理,负责收集日志并将其发送给 loki 。
  • Grafana ,用于 UI 。

个人理解

  • 轻存储:不对日志内容进行任何索引;存储可靠性依赖云端存储,如S3
  • 依赖计算:对日志的分析处理在查询的时候进行;可水平扩展,延迟跟节点数有关
  • 严格时间序:必须按照日志时间序进入系统,否则loki会丢弃日志

系统架构

Distributor

  • 负责处理客户端发送过来的日志流。检测日志的合法性,并将合法日志并行分发给合适(一致性hash)的Ingester

  • hash规则:tenant+lableset+副本数

Ingester

  • 负责写日志到后端存储;返回内存中缓存的日志数据给读请求

Querier

  • 负责处理LogQL语言的日志查询,从ingesters和后端存储获取日志返回

Query frontend:

  • 可选服务。提供Querier的API,通过切分请求,来加速读请求。

写流程

整体写入路径如下图所示:

各组件的处理过程如下

Distributor:做基本检测,转发给符合要求的ingester

  • 检查日志合法性
  • tanant+labelset做hash
  • 考虑三副本,发送给3个ingester

Ingester

  • 选择chunks
  • 压缩日志(gzip)
  • append to chunks

如果chunk写入满,怎么办?

  • 更新index
  • 创建新的chunk

整个写流程的官网介绍:

To summarize, the write path works as follows:

  1. The distributor receives an HTTP/1 request to store data for streams.
  2. Each stream is hashed using the hash ring.
  3. The distributor sends each stream to the appropriate ingesters and their replicas (based on the configured replication factor).
  4. Each ingester will create a chunk or append to an existing chunk for the stream’s data. A chunk is unique per tenant and per labelset.
  5. The distributor responds with a success code over the HTTP/1 connection.

读流程

查询条件

  • time_range,lable selector,可选条件

查询主要在Querier完成

  • 查询Index,获取匹配的chunks和保存未刷到后端存储的数据的Ingester

  • 从后端存储的chunk获取数据

  • 从Ingester获取数据

  • 根据查询条件处理数据

官网介绍:

To summarize, the read path works as follows:

  1. The querier receives an HTTP/1 request for data.
  2. The querier passes the query to all ingesters for in-memory data.
  3. The ingesters receive the read request and return data matching the query, if any.
  4. The querier lazily loads data from the backing store and runs the query against it if no ingesters returned data.
  5. The querier iterates over all received data and deduplicates, returning a final set of data over the HTTP/1 connection.

数据模型

  • Loki参考了Prometheus。数据由租户、标签、时间戳、内容组成;
  • 同一租户标签相同的日志属于同一个日志流
  • 不同的日志流对应不同的chunk存储

数据模型分两类介绍:

  • 数据协议
  • 存储模型

数据协议

存储模型

缓存

  • Ingester未flush到后端存储的数据
  • 存储:Ingester内存

chunk

  • 在后端存储的形式
  • 存储:Cassandra/GCS/FileSystem/S3/Notablementions(MinIO)

Index

  • 标签 -> 日志流 -> chunk的索引映射
  • 存储:Single Store(boltdb-shjpper)/Cassandra/BitTable/DynamoDB/DoltDB

存储模型之ingester缓存

模型结构:内存+多层树形结构

  • Instances:UserID为键Instance为值的Map结构
  • Instance:一个租户下素有日志流(stream)的容器
  • Streams:以日志流的指纹(streamFP)为键,Stream为值的Map结构
  • Stream:一个日志流所有Chunk的容器
  • Chunks:Chunk的列表,时间升序排列
  • Chunk:持久存储读写最小单元的内存态的结构
  • Block:Chunk的分块,为已压缩归档的数据
  • HeadBlock:尚在开放写入的分块,满足一定条件后归档为Block,新HeadBlock被创建
  • Entry:单挑日志内容,包含时间戳和日志内容

存储模型之chunk

各字段介绍:

  • meta:封装chunk所属stream的指纹、租户ID,开始截止时间等元信息
  • data:封装日志内容,其中一些重要字段含义如下
  • encode:数据的压缩格式
  • block-n bytes:保存一个block的日志数据
  • **#blocks section byte offset(4B): **记录#blocks单元的offset
  • #blocks:记录一共有多少个block
  • #entries:和block-n bytes一一对应,记录每个block里的日志行数、时间起止点,block的开始位置和长度等元信息

Chunk****数据的解析顺序:

  1. 根据尾部**#blocks** section byte offset单元得到#blocks位置
  2. 根据#blocks单元的出Chunk里的block数量
  3. 从#block单元位置开始解析所有block的元信息
  4. 顺序根据每个block元信息解析block数据

存储模型之index

index表结构

各字段简介

  • Table_N:根据时间周期分表名
  • hash:不用查询类型时使用的索引
  • range:范围查询字段
  • value: 日志标签的值
字段解释 index查询
- seriesID:日志流ID
- shard:分片, shard = seriesID % 分片数(可配置)
- userID:租户ID
- lableName:标签名
- labelVaue: 标签值
- labelValueHash:标签值hash
- chunkID:chunk的ID(cos中key)
- bucketID: 分桶,timestamp / secondsInDay(按天分割)
- **chunkThrough:**chunk里最后一条数据的时间
- metricName:固定为logs
图中四种颜色表示的索引类型从上到下分别为:

- 数据类型1: 用于根据用户ID搜索查询所有日志流的ID
- 数据类型2: 用户根据日志流ID搜索对应的所有标签名
- 数据类型3: 用于根据用户ID和标签查询日志流的ID
- 数据类型4: 用于根据用户ID日志流ID查询底层存储ChunkID

其中数据类型1和数据类型2用于查询Label;
数据类型3和数据类型4用户查询实际数据,这个是我们经常用的

查询语言LogQL

LogQL可以理解为一个分布式的grep日志聚合查看器,使用Label和operators进行过滤。详见LogQL,也可参考五分钟了解LogQL用法

LogQL查询分类:

  • Log queries
  • Metric queries(根据内容进行实时计算)

Log queries

  • log stream selector(required): 根据label的key和value进行selector

  • log pipeline(optional):进一步处理和过滤日志流

Metric queries

QueryFrontend并发查询

单个请求虽然可以直接调用querier API进行查询,但是容易大查询OOM。Loki引入QueryFrontend实现查询分解和加速

QueryFrontend拆分请求原理

  • 分割Request:将单个查询分割成子查询subReq列表
  • Feeder:将子查询顺序注入缓存队列BufQueue
  • Runner:多个并发的运行器将Buf
  • Queue中查询注入到子查询队列,等待返回查询结果
  • Querier通过gRPC协议实施从子查询队列弹出子查询,执行后将结果返回给相应的Runner
  • 所有子请求在Runner执行完毕后汇总结果返回API响应

Loki实践

环境搭建

搭建教程:https://www.cnblogs.com/fsckzy/p/13231696.html

  • 下载Promtail、Loki(v2.1.0)和Grafana并安装
  • 修改配置文件,启动程序
  • 本地文件系统为例
    Loki服务端:

采集端promtail:

查询前端grafana配置:

查询实践

Log Query

{job_type=“nginx_access”}

正则解析:{job_type=“nginx_access”} | regexp “^(?P<source_ip>[\d:]+) - - \[(?P[\w:/]+\s[+\-]\d{4})\] “(?P\S+)\s?(?P

\S+)?\s?(?P\S+)?” (?P<status_code>\d{3}|-) (?P.*)$” | status_code == 200

Metric Query

rate({job_type=“nginx_access”} [1m])

sum(rate({job_type=“nginx_access”} [1m])) by (host)

多租户和s3使用配置

多租户

Loki:

  • auth_enable: true

promtail

  • clients.tenant_id: "username”

grafana配置

  • 增加header:X-Scope-OrgID

s3配置

《Loki系统研究及实践.doc》

下载本文的Word格式文档,以方便收藏与打印。