容器环境中的日志分布在各个工作节点上,传统的登录节点或挂载共享存储的方式很难适应 Pod 的动态调度。Loki 借鉴 Prometheus 的标签模型,把日志组织成带标签的流,只对标签建立索引而不解析日志正文,这让写入延迟和存储开销都明显下降。Promtail 作为采集端部署在每个节点,自动发现 Pod 和容器,将带有命名空间、Pod 名称、容器名称等标签的日志推送到 Loki,再由 Grafana 统一查询,形成一套从采集到检索的完整链路。

一、Loki 的写入路径与标签策略
Loki 集群由分发器、摄取器和查询器组成,写入时 Promtail 先调用推送接口,分发器将日志按租户和标签哈希后发给摄取器,摄取器在内存中构建日志块并定期刷新到对象存储。对象存储里保存的是压缩后的日志块和对应的标签索引文件,查询时查询器通过标签索引定位到相关块,再对块内的日志正文做过滤。这种设计避免了对每行日志建立全文倒排索引,存储量通常只有 Elasticsearch 方案的几分之一。
标签是 Loki 查询的入口,但标签数量不是越多越好。每增加一个高基数标签,索引体积都会膨胀,查询也会变慢。推荐只保留与排查强相关的元数据,例如命名空间、Pod 名称、容器名称、节点名称。像用户 ID、请求 ID、设备编号这类取值非常多的字段,不应该作为标签,而应保留在日志正文中,查询时用 LogQL 的行过滤表达式来检索。
为了控制标签基数,可以在 Promtail 的 relabel 配置里显式选择需要的元数据,丢弃其余。下面的配置片段展示了从 Kubernetes Pod 元数据中抽取命名空间、Pod 名称和容器名称作为标签,其余标签默认不保留。
scrape_configs:
- job_name: kubernetes-pods
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels:
- __meta_kubernetes_namespace
target_label: namespace
- source_labels:
- __meta_kubernetes_pod_name
target_label: pod
- source_labels:
- __meta_kubernetes_pod_container_name
target_label: container
- action: labeldrop
regex: __meta_kubernetes_pod_label_.+
如果集群规模较大,还可以只保留几个核心标签,配合租户机制隔离不同业务线。租户通过请求头 X-Scope-OrgID 区分,Loki 会按租户分别存储和查询,适合多团队共用一个 Loki 实例。
二、部署 Promtail 采集容器日志
Promtail 通常以 DaemonSet 方式运行在每个节点上,需要挂载 /var/log/pods 和容器运行时的日志目录。Kubernetes 的 kubelet 会为每个容器在 /var/log/pods 下创建符号链接,指向容器运行时实际的日志文件。Promtail 使用 kubernetes_sd_configs 中的 pod 角色自动发现本节点的 Pod,并通过 relabel 生成标签,再调用 Loki 的推送接口。
容器运行时的日志格式不同,containerd 输出的 CRI 格式包含时间戳、stdout/stderr 标记和日志内容,需要经过 pipeline_stages 解析。Promtail 内置 cri 解析器,可以自动提取部分日志和流信息。下面的 YAML 展示了一个完整的 DaemonSet 配置要点,包含挂载路径、服务发现和解析阶段。
server:
http_listen_port: 9080
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: kubernetes-pods
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels:
- __meta_kubernetes_namespace
target_label: namespace
- source_labels:
- __meta_kubernetes_pod_name
target_label: pod
- source_labels:
- __meta_kubernetes_pod_container_name
target_label: container
pipeline_stages:
- cri: {}
实际部署时可以使用 Helm Chart 快速安装 Loki 和 Promtail。如果已有对象存储,可以将 Loki 的存储指向 S3 兼容服务,同时配置 compactor 定期压缩日志块。日志保留周期通过 Loki 的 limits_config 设置,例如 retention_period: 168h 表示日志保留 7 天。保留策略依赖 compactor 定期清理,因此单实例模式下需要开启 compactor 参数。
三、在 Grafana 中用 LogQL 查询日志
Grafana 内置 Loki 数据源,添加时只需填写 Loki 查询前端的地址。查询界面支持 LogQL 语句,最基本的查询是先用标签选择器锁定日志流,再用行过滤表达式筛选内容。例如在 default 命名空间下查找包含 error 的日志,可以写成 {namespace="default"} |= "error"。如果还想排除超时类错误,可以追加 != "timeout"。
LogQL 还支持聚合和数学运算。例如统计过去五分钟内错误日志的每秒速率,可以用 rate({job="kubernetes-pods"} |~ "error" [5m])。聚合函数 sum、max、avg 可以直接对日志流做统计,生成时间序列曲线,这在告警规则中很实用。下面的示例展示了按命名空间统计错误率。
sum by (namespace) (
rate({job="kubernetes-pods"} |~ "error" [5m])
)
除了在 Explore 页面交互查询,Grafana 告警也可以直接使用 LogQL 表达式。当错误率超过阈值时触发通知,这样日志系统既能查问题也能提前预警。对于结构化的 JSON 日志,LogQL 的 json 解析器可以把字段提取出来,便于按字段过滤或聚合,例如 | json | level="error"。
四、多行日志与常见调优
Java 应用抛出的异常堆栈、Goroutine 崩溃信息通常会跨越多行,如果不做处理,每一行都会被当成独立日志,排查时很难看到完整上下文。Promtail 的 multiline 阶段可以定义首行正则,将后续行合并到同一条日志。下面的配置以日期时间开头作为首行判断依据,适合大多数带时间戳的日志。
pipeline_stages:
- multiline:
firstline: '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}'
max_wait_time: 3s
多行合并会增加 Promtail 的内存使用,尤其是遇到长时间未闭合的日志块。max_wait_time 用来指定多久后强制结束当前块,避免一条异常堆栈持续占用缓冲区。对于已经由容器运行时按行输出的日志,不需要开启 multiline,否则反而可能把正常日志错误合并。
性能方面,如果单实例 Loki 写入压力过大,可以将 distributive、ingester、querier 拆分为独立组件,并使用 memberlist 做成员发现。对象存储建议启用版本控制和生命周期策略,与 Loki 的保留期配合使用。查询侧可以增加查询前端缓存,重复的统计查询会明显提速。最后要定期检查标签基数,避免把请求 ID 这类字段当作标签写入。