2. Loki 基础回顾:架构、存储模型与 LogQL
在深入 eBPF 网络观测与节点池巡检回退演练之前,必须牢固掌握 Loki 的核心设计理念。Loki 并非传统日志搜索引擎(如 ELK),而是受 Prometheus 启发、专为日志而生的“日志聚合系统”。其核心优势在于:不进行全文索引,仅对日志元数据(标签)建立索引,从而大幅降低存储成本与资源消耗。
2.1 Loki 整体架构
Loki 采用微服务架构,主要组件包括 Distributor、Ingester、Querier 和 Compactor。下图展示了日志从采集到查询的完整链路:
架构要点:Loki 将写路径(Distributor → Ingester → 对象存储)与读路径(Querier → 对象存储 / Ingester)分离,支持水平扩展。Compactor 在后台异步优化存储布局,对查询透明。
2.2 日志存储模型
Loki 的存储模型围绕 Stream(日志流)和 Chunk(数据块)两个核心概念构建:
| 概念 | 说明 | 关键属性 |
|---|---|---|
| Stream | 一组具有相同标签集合(Label Set)的日志条目。例如:{job="nginx", instance="10.0.0.1"} 定义了一个唯一的日志流。 |
由 Label Set 唯一标识;每个 Stream 内部日志按时间戳严格排序。 |
| Chunk | 属于同一 Stream 的一段连续日志数据,经过压缩后存储。Chunk 是 Loki 的最小存储单元。 | 包含起始/结束时间戳、压缩后的日志内容、元数据(如 CRC 校验)。 |
| Index | 仅对 Stream 的标签建立倒排索引,不索引日志内容。支持基于标签的高效过滤。 | 存储于 BoltDB 或 TSDB(自 Loki 2.8+),支持按标签名和值快速定位 Stream。 |
| Object Store | Chunk 和 Index 的持久化后端,支持 S3、GCS、Azure Blob、MinIO 等。 | 按时间分桶(Bucket),每个 Bucket 内包含多个 Chunk 文件和索引文件。 |
设计哲学:Loki 不解析日志内容,因此标签设计至关重要。合理的标签(如
job、namespace、pod)能大幅提升查询性能;避免使用高基数标签(如 user_id、request_id),否则会导致索引膨胀。
2.3 LogQL 查询语言基础
LogQL 是 Loki 的查询语言,语法受 PromQL 和 SQL 启发。它分为两个核心部分:日志流选择器和日志管道。
2.3.1 日志流选择器
选择器用于过滤出特定的日志流,语法与 Prometheus 标签匹配器一致:
# 选择 job 为 nginx 的所有日志流
{job="nginx"}
# 选择 namespace 为 production,且 pod 名以 web- 开头的日志流
{namespace="production", pod=~"web-.*"}
# 选择 instance 不等于 10.0.0.1 的日志流
{job="api-server", instance!="10.0.0.1"}
2.3.2 日志管道操作
管道操作符对选中的日志行进行进一步处理、过滤或提取:
| 操作符 | 作用 | 示例 |
|---|---|---|
|= |
行包含(子串匹配) | {job="nginx"} |= "error" |
!= |
行不包含 | {job="nginx"} != "healthcheck" |
|~ |
行匹配正则表达式 | {job="api"} |~ "5[0-9]{2}" |
!~ |
行不匹配正则 | {job="api"} !~ "DEBUG|TRACE" |
\| json |
解析 JSON 格式日志行,提取字段 | {job="api"} | json | status_code > 500 |
\| logfmt |
解析 logfmt 格式日志行 | {job="app"} | logfmt | level="error" |
\| regexp |
使用命名正则提取字段 | {job="web"} | regexp "(?P<method>\w+) (?P<path>/\S+)" |
\| unwrap |
从日志行中提取数值用于聚合 | {job="api"} | json | unwrap duration_ms | avg() |
2.3.3 聚合与指标查询
LogQL 支持将日志转换为指标(Metric Query),语法与 PromQL 高度兼容:
# 统计过去 5 分钟每个 job 的错误日志数量
sum by (job) (count_over_time({job=~".+"} |= "error" [5m]))
# 计算 P99 响应延迟(假设日志中包含 duration_ms 字段)
quantile_over_time(0.99, {job="api"} | json | unwrap duration_ms [1h]) by (instance)
# 计算错误率(错误日志 / 总日志)
sum(rate({job="nginx"} |= "5[0-9]{2}" [5m])) / sum(rate({job="nginx"}[5m]))
性能注意事项:
- 优先使用
|=和!=进行简单过滤,避免在管道中使用高开销的正则表达式。 - 聚合查询(如
rate、count_over_time)的步长(Step)不宜过小,建议 ≥ 15s。 - 使用
unwrap提取数值时,确保字段类型一致,避免类型转换错误导致查询失败。