Loki,真香!

Loki 是一个水平可扩展、高可用、多租户的日志存储与查询系统。受 Prometheus 启发,Loki 不对日志内容进行索引,而是采用标签(Label)作为索引,从而降低存储成本。

Like Prometheus, but for logs.

架构设计

Loki 采用读写分离架构,由多个微服务构建而成,被设计成一个水平可扩展的分布式系统。

picture.image

其中写组件有:

1、Distributor 分发器:分发器通过 HTTP 接收到日志后,会进行验证(验证日志时间或日志行大小等是否满足规则)、预处理(对 labels 按照 key 以字典顺序排序,方便后续进行一致性 hash 算法来将日志发往 Ingester 接收器),同时还负责限速功能,流量过大时可以拒绝额外的请求。

2、Ingester 接收器:接收器是一个有状态的组件,在日志流进入时对其进行 gzip/snappy 压缩操作,并负责构建和刷新日志 chunk ,当内存中的 chunk 达到一定的数量或者时间后,就会刷新 chunk 和对应的 index 索引存储到本地文件系统或对象存储中。接收器默认会启用 WAL 功能,防止数据丢失。

读组件有:

1、Query Frontend 查询前端:查询前端是一个可选的组件,具有查询拆分、缓存的作用。一个查询可以拆解成多个小查询,并行在多个 Querier 组件上进行查询,最终合并返回给前端展示。查询前端内部有一个内存队列,还可以将其移出作为一个 Query Scheduler 查询调度器的单独进程运行。

2、Querier 查询器:接收一个时间范围和标签选择器,Querier 查询器根据 index 索引来确定哪些日志 chunk 匹配,然后将结果显示出来。在查询数据时,优先查询所有 Ingester 接收器中的内存数据,没查到再去查存储。如果开启了数据副本,数据可能重复,因此 Querier 还有数据去重的功能。

另外还有一些其它组件:

1、Ruler 规则器:负责日志告警功能,可以持续查询一个 rule 规则,并将超过阈值的事件推送给 AlertManager 或者其它告警 Webhook 服务。

2、Compactor 压缩器:负责定时对索引进行压缩合并,同时负责日志的删除功能。

3、Memcaches 缓存:这部分属于外部第三方组件,支持的缓存类型有 in-memory、redis 和 memcached ,可以在 Ingester 、Query Frontend 、Querier 和 Ruler 上配置 Results 查询结果缓存、Index 索引缓存或 Chunks 块缓存。

组件之间的交互流程大致如下:

picture.image

一致性哈希环设计

Loki 的分布式架构源自 https://github.com/cortexproject/cortex 项目,对于各组件服务状态和数据的通信均采用一致性哈希环设计,哈希环的配置支持 consul、etcd、inmemory 或 memberlist :


        
        
            

          
 common:
 
          
   

 
            
          
 ring:
 
          
   

 
              
          
 kvstore:
 
          
   

 
                
          
 # 支持切换为 consul、etcd、inmemory
 
          
   

 
                
          
 store:
 
           
          
 memberlist
 
          
   

 
        
      

在 Loki 中定义了很多哈希环:IngesterRing、RulerRing 和 CompactorRing 等,以分发器(Distributor)和接收器(Ingester)组件为例,借助 IngesterRing 哈希环,Distributor 就可以确定日志流应该发往哪个 Ingester 实例,具体流程如下:

1、日志流的唯一性计算:每个日志流由租户 ID 及其所有标签的 key/value 组合唯一确定,并由此计算出日志流的 hash key —— 一个无符号的 32 位整数。

2、Ingester 的注册:每个 Ingester 实例会在哈希环(IngesterRing)上注册自身,并分配一组 token(每个 token 为随机生成的无符号 32 位整数),用于确定其在哈希环中的位置。

picture.image

3、日志流的分配:当 Distributor 需要分发日志时,它会在哈希环上找到第一个大于日志流 hash key 的 token,并将该 token 所属的 Ingester 实例视为目标存储节点。

a. 副本机制:若数据副本数(replication_factor) 大于 1(默认为 3),则继续顺时针查找,找到下一个 token 对应的不同 Ingester 实例,确保日志的多副本存储,提高容错能力。

b. 状态约束:仅当目标 Ingester 处于 JOINING 或 ACTIVE 状态时,才能接收日志写入请求;仅当 Ingester 处于 ACTIVE 或 LEAVING 状态时,才能处理日志读取请求。

其动态演示效果如下:

picture.image

但在这种机制下,若单个日志流中的数据量过大,就容易导致 Ingester 实例负载不均衡,如下:

picture.image

此时,可以启用自动分片流功能,通过在现有日志流中自动添加 \_\_stream\_shard\_\_ 标签及其值,以控制日志流速保持在 desired\_rate 以下,达到负载均衡的效果:


        
        
            

          
 limits\_config:
 
          
   

 
            
          
 shard\_streams:
 
          
   

 
              
          
 enabled:
 
           
          
 true
 
          
   

 
              
          
 desired\_rate:
 
           
          
 1536KB
 
          
   

 
        
      

picture.image

存储设计

在 Loki 中,标签(label)实际就是在提取日志时分配给日志的一组任意 key/value,既是 Loki 对传入数据进行分块的键,也是查询时用于查找日志的索引。

每个标签的 key 和 value 的组合会唯一定义成一个日志流(stream),哪怕仅有一个标签值发生了变化,都会重新创建一个新的日志流。

不同的日志流会在 Ingester 实例的内存中构建出不同的日志 chunk ,满足规则(达到 chunk\_target\_sizemax\_chunk\_agechunk\_idle\_period 上限)就会刷新到对象存储或本地文件系统中:

picture.image

因此,Loki 需要存储两种不同类型的数据:块(chunk)和索引(index)。

  • chunk:即日志本身,一个 chunk 包含很多 block ,进行压缩后存储
  • index:即日志索引,key/value 结构,key 是日志 label 的哈希,value 则包含日志存在哪个 chunk 上、chunk 大小、日志的时间范围等信息

其中 chunk 的存储可以直接上传到配置的存储系统中(例如本地文件系统或 S3),而 index 的存储处理稍微麻烦些。

在 2.0 之前,chunk 和 index 的存储是分开的,意味着需要配置两个存储系统。而 2.0 开始,推行一种叫单一存储架构的设计,实现了 “boltdb-shipper” 索引存储,这种机制下只需要一个共享存储,例如 S3,就可以同时用于 chunk 和 index 的存储。到了 Loki 2.8 ,再次推出了更高效的 “tsdb-shipper” 索引存储,这也是目前 3.x 版本所推荐的索引存储方式。

这两种索引存储的原理大致上是相似的,工作可以分为 uploadsManager 和 downloadsManager 两部分:

  • uploadsManager:负责上传 active\_index\_directory 内的索引分片到配置的共享存储中,同时负责定期清理工作
  • downloadsManager:负责从共享存储下载索引到本地缓存目录 cache\_location ,同时负责定期同步和清理工作

简单来理解就是,一个是把本地 boltdb 文件当作索引存储,另一个把本地 tsdb 文件当作索引存储,但这两种索引存储都有 “shipper” 的能力,可以把自身上传到配置的共享存储中,并保持同步。如此一来,我们就可以利用 S3 同时存储 chunk 和 index 了。

随着版本的迭代,不可避免会出现很多不同的存储模式,好在 Loki 允许通过日期起点来定义不同时间段使用不同的存储模式:


        
        
            

          
 schema\_config:
 
          
   

 
            
          
 configs:
 
          
   

 
              
          
 -
 
          
 from:
 
          
 2024
 
          
 -01
 
          
 -01
 
          
   

 
                
          
 store:
 
          
 boltdb-shipper
 
          
   

 
                
          
 object\_store:
 
          
 s3
 
          
   

 
                
          
 schema:
 
          
 v12
 
          
   

 
                
          
 index:
 
          
   

 
                  
          
 prefix:
 
          
 index\_
 
          
   

 
                  
          
 period:
 
          
 24h
 
          
   

 
              
          
 -
 
          
 from:
 
          
 2025
 
          
 -01
 
          
 -01
 
          
   

 
                
          
 store:
 
          
 tsdb
 
          
   

 
                
          
 object\_store:
 
          
 s3
 
          
   

 
                
          
 schema:
 
          
 v13
 
          
   

 
                
          
 index:
 
          
   

 
                  
          
 prefix:
 
          
 index\_
 
          
   

 
                  
          
 period:
 
          
 24h
 
          
   

 
        
      

需要注意的是:

  1. 升级架构,始终将新模式中的 from 日期设置为未来的日期,要注意是从 UTC 00:00:00 开始。
  2. 全新部署,from 日期需要设置为以前的日期,才可以接收处理日志。
  3. 架构变更是无法撤销或回滚的,使用什么架构写入的数据只能由该架构读取。

查询设计

Loki 第一次查询时,Querier 查询器会从共享存储中下载查询时间范围内的索引并解压到本地缓存目录 cache\_location ,并按 resync\_interval 周期同步,该索引缓存有效期受 cache\_ttl 配置控制。这部分工作由之前介绍的索引存储的 downloadsManager 完成,这也是为什么 Loki 的第一次查询会比较慢。

抛开拉取索引耗时这部分因素,在 Loki 中,查询从快到慢分别为:

picture.image

  • Label matchers 标签匹配器(最快):直接基于索引匹配到块,查找出满足 limit 的日志条数
  • Line filters 行过滤器(中等):把满足标签匹配器匹配到的块,再进行过滤,直到查找出满足 limit 的日志条数
  • Label filters 标签过滤器(最慢):把满足标签匹配器匹配到的块,进行二次标签,然后再进行过滤,直到查找出满足 limit 的日志条数

以行过滤器的查询流程为例:

1、时间范围拆分:Query Frontend 首先根据 split\_queries\_by\_interval 将查询拆分为多个较小的时间段。例如,一个跨度为 4 小时的查询可能被拆分为 4 个独立的 1 小时子查询,这种拆分可以并行处理不同时间段的数据。

picture.image

2、动态分片:Query Frontend 继续将每个时间段的子查询进行进一步的动态分片。分片数量取决于数据量,数据量大的子查询可能拆分为更多分片,而数据量小的可能仅少量分片。分片的目的是将 chunk 按日志流的标签进一步细分,从而提升并行处理效率。

picture.image

3、任务队列与并行处理:Query Frontend 将拆分后的分片任务提交至 Query Scheduler 任务队列中,根据公平调度策略将任务分配给空闲的 Querier 工作节点并行处理。Querier 会从 Ingester 中的内存数据或对象存储中拉取对应的数据块,解析并过滤日志内容,最终返回匹配的结果。

picture.image

4、结果合并:所有子查询和分片的结果会被汇总到 Query Frontend 组件,进行排序、去重和合并,最终返回完整的查询结果。

完整的查询流程如下:

picture.image

所以 Loki 的设计就是推荐使用并行化 (parallelization) 来实现最佳性能,将查询分解成小块,并将其并行调度,这样就可以在小时间内查询大量的日志数据。

最后欢迎加入苏三的星球,你将获得:DeepSeek相关技术、商城系统实战、秒杀系统实战、代码生成工具、系统设计、性能优化、技术选型、高频面试题、底层原理、Spring源码解读、工作经验分享、痛点问题等多个优质专栏。

还有1V1答疑、修改简历、职业规划、送书活动、技术交流。

picture.image

目前星球已经更新了4900+篇优质内容,还在持续爆肝中.....

星球已经被官方推荐了3次,收到了小伙伴们的一致好评。戳我加入学习,已有1600+小伙伴加入学习。

此外,苏三最近建了一个DeepSeek交流群,欢迎一些志同道合的小伙伴扫描进群:

picture.image

也可以直接加我微信,备注:deepseek,即可进群。

picture.image

picture.image

0
0
0
0
评论
未登录
暂无评论