背景
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:
- The distributor receives an HTTP/1 request to store data for streams.
- Each stream is hashed using the hash ring.
- The distributor sends each stream to the appropriate ingesters and their replicas (based on the configured replication factor).
- 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.
- 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:
- The querier receives an HTTP/1 request for data.
- The querier passes the query to all ingesters for in-memory data.
- The ingesters receive the read request and return data matching the query, if any.
- The querier lazily loads data from the backing store and runs the query against it if no ingesters returned data.
- 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****数据的解析顺序:
- 根据尾部**#blocks** section byte offset单元得到#blocks位置
- 根据#blocks单元的出Chunk里的block数量
- 从#block单元位置开始解析所有block的元信息
- 顺序根据每个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实践
环境搭建
搭建教程:
- 下载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配置