再见,ELK( 二 )


低存储成本
只索引与日志相关的元数据标签 , 而日志内容则以压缩方式存储于对象存储中, 不做任何索引 。
相较于 ES 这种全文索引的系统 , 数据可在十倍量级上降低 , 加上使用对象存储 , 最终存储成本可降低数十倍甚至更低 。
方案不解决复杂的存储系统问题 , 而是直接应用现有成熟的分布式存储系统 , 比如 S3、GCS、Cassandra、BigTable。
Loki 架构
整体上 Loki 采用了读写分离的架构 , 由多个模块组成:

  • Promtail、Fluent-bit、Fluentd、Rsyslog 等开源客户端负责采集并上报日志 。
  • Distributor:日志写入入口 , 将数据转发到 Ingester 。
  • Ingester:日志的写入服务 , 缓存并写入日志内容和索引到底层存储 。
  • Querier:日志读取服务 , 执行搜索请求 。
  • QueryFrontend:日志读取入口 , 分发读取请求到 Querier 并返回结果 。
  • Cassandra/BigTable/DnyamoDB/S3/GCS:索引、日志内容底层存储 。
  • Cache:缓存 , 支持 redis/Memcache/本地 Cache 。
其主体结构如下图所示:
再见,ELK

文章插图
 
Distributor:作为日志写入的入口服务 , 其负责对上报数据进行解析、校验与转发 。
它将接收到的上报数解析完成后会进行大小、条目、频率、标签、租户等参数校验 , 然后将合法数据转发到 Ingester 服务 , 其在转发之前最重要的任务是确保同一日志流的数据必须转发到相同 Ingester 上 , 以确保数据的顺序性 。
Hash 环:Distributor 采用一致性哈希与副本因子相结合的办法来决定数据转发到哪些 Ingester 上 。
Ingester 在启动后 , 会生成一系列的 32 位随机数作为自己的 Token  , 然后与这一组 Token 一起将自己注册到 Hash 环中 。
在选择数据转发目的地时 , Distributor 根据日志的标签和租户 ID 生成 Hash , 然后在 Hash 环中按 Token 的升序查找第一个大于这个 Hash 的 Token  , 这个 Token 所对应的 Ingester 即为这条日志需要转发的目的地 。
如果设置了副本因子 , 顺序的在之后的 Token 中查找不同的 Ingester 做为副本的目的地 。
Hash 环可存储于 etcd、consul 中 。另外 Loki 使用 Memberlist 实现了集群内部的 KV 存储 , 如不想依赖 etcd 或 consul  , 可采用此方案 。
输入输出:Distributor 的输入主要是以 HTTP 协议批量的方式接受上报日志 , 日志封装格式支持 JSON 和 PB  , 数据封装结构:
[{"stream": {"label1": "value1","label1": "value2"},"values": [["<timestamp nanoseconds>","log content"],["<timestamp nanoseconds>","log content"]]},...... ] Distributor 以 grpc 方式向 ingester 发送数据 , 数据封装结构:
{"streams": [{"labels": "{label1=value1, label2=value2}","entries": [{"ts": <unix epoch in nanoseconds>, "line:":"<log line>" },{"ts": <unix epoch in nanoseconds>, "line:":"<log line>" },]}....] } ①Ingester
作为 Loki 的写入模块 , Ingester 主要任务是缓存并写入数据到底层存储 。根据写入数据在模块中的生命周期 , ingester 大体上分为校验、缓存、存储适配三层结构 。
②校验
Loki 有个重要的特性是它不整理数据乱序 , 要求同一日志流的数据必须严格遵守时间戳单调递增顺序写入 。
所以除对数据的长度、频率等做校验外 , 至关重要的是日志顺序检查 。
Ingester 对每个日志流里每一条日志都会和上一条进行时间戳和内容的对比 , 策略如下:
  • 与上一条日志相比 , 本条日志时间戳更新 , 接收本条日志 。
  • 与上一条日志相比 , 时间戳相同内容不同 , 接收本条日志 。
  • 与上一条日志相比 , 时间戳和内容都相同 , 忽略本条日志 。
  • 与上一条日志相比 , 本条日志时间戳更老 , 返回乱序错误 。
③缓存
日志在内存中的缓存采用多层树形结构对不同租户、日志流做出隔离 。同一日志流采用顺序追加方式写入分块:


推荐阅读