再见,ELK( 五 )

!= , =~ , !~ , 工作过程如下:
以如下 SQL 所描述的语义查询出标签选择器里引用的每个标签所对应 seriesID 集合 。
SELECT * FROM Table_N WHERE hash = ? hash 为租户 ID(userID)、分桶(bucketID)、标签名(labelName) 。
根据标签选择语法对每个 seriesID 集合进行过滤 。
将过滤后的集合进行并集、交集等操作求最终集合 。
比如 , {file~="MySQL*", level!="error"} 的工作过程如下:

  • 查询出标签“file”和标签"level"对应的 seriesID 的集合 , S1、S2 。
  • 求出 S1 中 file 的值匹配 mysql*的子集 SS1 , S2 中 level 的值!="error"的子集 SS2 。
  • 计算最终 seriesID 集合 S = SS1∩SS2 。
以如下 SQL 所描述的语义查询出所有日志流所包含的 chunk 的 ID:
SELECT * FROM Table_N Where hash = ? hash 为分桶(bucketID)和日志流(seriesID)计算的哈希值 。
根据 chunkID 列表生成遍历器来顺序读取日志行:遍历器作为数据读取的组件 , 其主要功能为从存储系统中拉取 chunk 并从中读取日志行 。其采用多层树形结构 , 自顶向下逐层递归触发方式弹出数据 。
再见,ELK

文章插图
 
具体结构如上图所示:
  • batch Iterator:以批量的方式从存储中下载 chunk 原始数据 , 并生成 iterator 树 。
  • stream Iterator:多个 stream 数据的遍历器 , 其采用堆排序确保多个 stream 之间数据的保序;
  • chunks Iterator:多个 chunk 数据的遍历器 , 同样采用堆排序确保多个 chunk 之间保序及多副本之间的去重 。
  • blocks Iterator:多个 block 数据的遍历器 。
  • block bytes Iterator:block 里日志行的遍历器 。
从 Ingester 查询在内存中尚未写入到存储中的数据:由于 Ingester 是定时的将缓存数据写入到存储中 , 所以 Querier 在查询时间范围较新的数据时 , 还会通过 grpc 协议从每个 ingester 中查询出内存数据 。
需要在 ingester 中查询的时间范围是可配置的 , 视 ingester 缓存数据时长而定 。
上面是日志内容查询的主要流程 。至于指标查询的流程与其大同小异 , 只是增加了指标计算的遍历器层用于从查询出的日志计算指标数据 。其他两种则更为简单 , 这里不再详细展开 。
QueryFrontend:Loki 对查询采用了计算后置的方式 , 类似于在大量原始数据上做 grep , 所以查询势必会消耗比较多的计算和内存资源 。
如果以单节点执行一个查询请求的话很容易因为大查询造成 OOM、速度慢等性能瓶颈 。
为解决此问题 , Loki 采用了将单个查询分解在多个 querier 上并发执行方式 , 其中查询请求的分解和调度则由 queryFrontend 完成 。
再见,ELK

文章插图
 
queryFrontend 在 Loki 的整体架构上处于 querier 的前端 , 它作为数据读取操作的入口服务 , 其主要的组件及工作流程如上图所示:
  • 分割 Request:将单个查询分割成子查询 subReq 的列表 。
  • Feeder:将子查询顺序注入到缓存队列 Buf Queue 。
  • Runner:多个并发的运行器将 Buf Queue 中的查询并注入到子查询队列 , 并等待返回查询结果 。
  • Querier 通过 grpc 协议实时从子查询队列弹出子查询 , 执行后将结果返回给相应的 Runner 。
  • 所有子请求在 Runner 执行完毕后汇总结果返回 API 响应 。
⑨查询分割
queryFrontend 按照固定时间跨度将查询请求分割成多个子查询 。比如 , 一个查询的时间范围是 6 小时 , 分割跨度为 15 分钟 , 则查询会被分为 6*60/15=24 个子查询 。
⑩查询调度
Feeder:Feeder 负责将分割好的子查询逐一的写入到缓存队列 Buf Queue , 以生产者/消费者模式与下游的 Runner 实现可控的子查询并发 。
Runner:从 Buf Queue 中竞争方式读取子查询并写入到下游的请求队列中 , 并处理来自 Querier 的返回结果 。
Runner 的并发个数通过全局配置控制 , 避免因为一次分解过多子查询而对 Querier 造成巨大的徒流量 , 影响其稳定性 。
子查询队列:队列是一个二维结构 , 第一维存储的是不同租户的队列 , 第二维存储同一租户子查询列表 , 它们都是以 FIFO 的顺序组织里面的元素的入队出队 。


推荐阅读