返回首页

7.2 Logging:Loki + Promtail

本文是 Unit 7「可观测性篇」的第 2 章,聚焦 Kubernetes 日志体系的设计与落地。

本章你会学到什么

  • 为什么日志采集不能只依赖 kubectl logs
  • Loki 与 Promtail 的基本架构和数据流
  • 容器日志如何采集、打标签、检索
  • 如何做留存策略、成本控制与排障实践

1. 为什么日志采集不能只依赖 kubectl logs

Kubernetes 官方日志架构文档明确说明:容器运行时会把应用输出重定向到 stdout / stderr,并以 CRI logging format 与 kubelet 对接;随后 kubelet 再通过 Kubernetes API 把日志提供给 kubectl logs 使用。也就是说,kubectl logs 本质上更像是查看某个容器当前日志文件的入口,而不是完整的日志平台。::cite[193]

它的问题主要有 4 个:

  1. 只适合临时查看:你得知道 Pod 名称,还要手工进入具体命名空间、具体容器。::cite[418]
  2. 保留能力很弱:Kubernetes 文档指出,kubectl logs 只会返回最新日志文件的内容;如果容器轮转、Pod 被驱逐,原始日志并不会天然长期保留。::cite[193]
  3. 不适合跨 Pod、跨节点聚合:当副本很多时,你很难用 kubectl logs 高效比对多个实例的共同异常。::cite[193]
  4. 不适合检索与分析:你无法像日志平台那样按照 namespaceapppod、错误关键字或时间范围做聚合查询。

换句话说,kubectl logs 很适合“快速看一下某个 Pod 的输出”,但不适合承担留存、聚合、检索、统计、审计、回放这些日志平台职责。

2. Kubernetes 中容器日志的真实路径

理解 Loki 之前,先把 Kubernetes 原生日志路径理顺:

  1. 应用把日志写到 stdout / stderr
  2. 容器运行时按 CRI 格式写入节点本地日志文件
  3. kubelet 管理日志轮转与访问
  4. kubectl logs 通过 Kubernetes API 拉取单个容器日志
  5. 节点级日志采集器再把这些日志统一送入外部日志系统

Kubernetes 官方把这种做法称为 cluster-level logging:也就是在每个节点上运行一个日志代理,统一把容器日志送到后端存储。::cite[193]

3. Loki 与 Promtail 的基本架构

Grafana Loki 官方文档把 Loki 定义为一个高可用、可横向扩展、多租户的日志聚合系统。它与传统日志系统最大的不同是:Loki 不索引日志正文,只索引日志流的标签(labels)元数据,因此在 Kubernetes 这类标签丰富、实例众多的环境里,通常能以更低成本完成日志检索。::cite[451]

Loki 的典型数据流可以理解成下面 3 段:

  1. Promtail / Agent:运行在节点侧,负责读取容器日志文件、解析、加标签、转发
  2. Loki:负责接收日志流、索引标签、存储日志块、处理查询
  3. Grafana:负责查询、展示、联动分析和可视化

Loki 文档也强调,日志流(log stream)是“一组共享相同 labels 的日志集合”,标签质量直接影响查询效率。::cite[451]

3.1 一个必须知道的现实:Promtail 已停更演进

Grafana 官方文档已经标注:Promtail 已被弃用,处于 LTS(长期支持)阶段,并已进入 EOL 节点;新能力会更多落到 Grafana Alloy 等采集端。 但由于大量历史方案、社区文章和教学环境仍在使用 Promtail,本章仍按用户要求使用 Loki + Promtail 讲解,同时你需要知道:新生产环境应评估 Alloy 或其他采集代理的替代路线。::cite[358]

3.2 Loki 的部署模式

Loki 架构文档说明,Loki 采用微服务式设计,但也支持单二进制部署;入门阶段最容易上手的是 single binary,规模上来之后再切到 simple scalable deployment,把组件拆成 read、write、backend 三部分。::cite[449]

所以教程里可以分两种心态:

  • 学习环境:单副本、单二进制、先把链路跑通
  • 生产环境:对象存储 + compactor + 可扩展读写层 + 留存策略 + 高可用

4. 实操:用 Loki + Promtail 采集 Kubernetes 容器日志

下面给出一套适合学习环境理解原理的最小示例。

4.1 部署 Loki(单副本示例)

apiVersion: v1
kind: Namespace
metadata:
  name: logging
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: loki-config
  namespace: logging
data:
  loki.yaml: |
    auth_enabled: false

    server:
      http_listen_port: 3100

    common:
      path_prefix: /loki
      replication_factor: 1
      ring:
        kvstore:
          store: inmemory

    schema_config:
      configs:
        - from: 2024-01-01
          store: tsdb
          object_store: filesystem
          schema: v13
          index:
            prefix: loki_index_
            period: 24h

    storage_config:
      filesystem:
        directory: /loki/chunks

    limits_config:
      retention_period: 168h

    compactor:
      working_directory: /loki/compactor
      retention_enabled: true
      delete_request_store: filesystem
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: loki
  namespace: logging
spec:
  replicas: 1
  selector:
    matchLabels:
      app: loki
  template:
    metadata:
      labels:
        app: loki
    spec:
      containers:
        - name: loki
          image: grafana/loki:3.0.0
          args:
            - -config.file=/etc/loki/loki.yaml
          ports:
            - containerPort: 3100
              name: http
          volumeMounts:
            - name: config
              mountPath: /etc/loki
            - name: data
              mountPath: /loki
      volumes:
        - name: config
          configMap:
            name: loki-config
        - name: data
          emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
  name: loki
  namespace: logging
spec:
  selector:
    app: loki
  ports:
    - name: http
      port: 3100
      targetPort: 3100

说明:教学环境里用 emptyDir 足够理解流程;生产中应改成对象存储或持久卷,并设计副本与高可用策略。

4.2 部署 Promtail(DaemonSet 采集节点容器日志)

Kubernetes 官方推荐 cluster-level logging 的思路是“每个节点部署一个日志代理”。在 Loki 方案里,这通常由 Promtail DaemonSet 来承担。::cite[193]

apiVersion: v1
kind: ServiceAccount
metadata:
  name: promtail
  namespace: logging
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: promtail-config
  namespace: logging
data:
  promtail.yaml: |
    server:
      http_listen_port: 9080
      grpc_listen_port: 0

    positions:
      filename: /run/promtail/positions.yaml

    clients:
      - url: http://loki.logging.svc.cluster.local:3100/loki/api/v1/push

    scrape_configs:
      - job_name: kubernetes-pods
        pipeline_stages:
          - cri: {}
        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
          - source_labels: [__meta_kubernetes_pod_label_app]
            target_label: app
          - source_labels: [__meta_kubernetes_pod_node_name]
            target_label: node
          - action: replace
            replacement: /var/log/pods/*$1/*.log
            separator: /
            source_labels:
              - __meta_kubernetes_pod_uid
              - __meta_kubernetes_pod_container_name
            target_label: __path__
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: promtail
  namespace: logging
spec:
  selector:
    matchLabels:
      app: promtail
  template:
    metadata:
      labels:
        app: promtail
    spec:
      serviceAccountName: promtail
      containers:
        - name: promtail
          image: grafana/promtail:3.0.0
          args:
            - -config.file=/etc/promtail/promtail.yaml
          volumeMounts:
            - name: config
              mountPath: /etc/promtail
            - name: varlog
              mountPath: /var/log
            - name: positions
              mountPath: /run/promtail
      volumes:
        - name: config
          configMap:
            name: promtail-config
        - name: varlog
          hostPath:
            path: /var/log
        - name: positions
          emptyDir: {}

4.3 在 Grafana 中配置数据源

如果 Grafana 已经安装,只需要新增一个 Loki 数据源:

  • URL:http://loki.logging.svc.cluster.local:3100
  • Access:Cluster 内通常用 Server / Proxy 模式
  • 保存后用 LogQL 测试:{namespace="default"}

5. 标签组织:Loki 好不好用,关键看 labels 设计得好不好

Loki 官方文档强调,Loki 检索依赖标签而不是正文索引,因此标签设计就是日志体系的“主键设计”。标签太少,检索定位慢;标签太多,尤其高基数标签太多,会带来存储、索引与查询成本问题。::cite[451]

5.1 入门阶段建议保留的基础标签

建议优先保留这些相对稳定、查询价值高的标签:

标签 用途
cluster 多集群场景区分来源
namespace 按业务或环境划分
app 业务应用聚合
pod 定位具体实例
container 区分 Pod 内多个容器
node 判断是否节点局部故障
stream 区分 stdout / stderr

5.2 哪些标签不要轻易加

下面这些信息通常不建议直接做 Loki labels

  • request-id
  • trace-id(更适合放日志字段,再通过派生字段联动)
  • user-id
  • URL 全路径
  • 动态错误消息全文
  • Pod UID(除非你确实要按实例精确回放)

原因很简单:这些字段变化太快,基数太高,一旦做成标签,会让 Loki 的索引与查询代价迅速上升。

5.3 一个推荐思路:稳定维度做标签,变化内容放正文

你可以记一个经验法则:

  • 稳定、常用于筛选的字段 → 做 labels
  • 变化频繁、用于阅读和上下文的字段 → 放日志正文或 structured metadata

6. LogQL 检索方式:别只会全文 grep

Loki 最常用的检索有两步:

  1. 先按标签选中日志流
  2. 再按关键字、正则或解析后的字段做过滤

6.1 常见查询示例

按命名空间查看:

{namespace="prod"}

按应用查看错误日志:

{namespace="prod", app="checkout"} |= "ERROR"

按容器查看超时:

{namespace="prod", container="api"} |= "timeout"

只看 stderr:

{namespace="prod", app="checkout", stream="stderr"}

筛选 5xx 相关日志:

{namespace="prod", app="gateway"} |~ "5[0-9][0-9]"

6.2 检索日志时的推荐顺序

实战里,建议按这个顺序缩小范围:

  1. 先选 namespace
  2. 再选 app
  3. 再选 podcontainer
  4. 最后再按关键字过滤

这样查询会更快,也更容易避免把无关日志一起扫进来。

7. 留存策略与成本控制

Loki 官方文档指出,日志留存由 Compactor 负责;如果没有开启 compactor.retention-enabled,日志默认会一直保留。文档还指出,可以按 tenant 或 stream 维度定义更细粒度的留存策略。::cite[464]

这意味着:如果你不主动设计日志留存,日志成本只会越来越高。

7.1 留存设计建议

可以按日志价值分层:

日志类型 建议保留时长 说明
核心生产错误日志 30~90 天 便于事故回溯与审计
普通应用日志 7~30 天 满足日常排障即可
调试级日志 1~7 天 仅在问题排查期短暂开启
访问日志全文 3~14 天 高流量场景尤其要控制

7.2 成本控制的几个抓手

  1. 降低高基数标签:这是 Loki 成本优化里最关键的一步。::cite[451]
  2. 减少无意义 Debug 日志:别把“开发期日志级别”原封不动搬进生产。
  3. 分环境留存:生产、预发、测试的保留时长不应该一样。
  4. 热点日志单独治理:访问日志、审计日志、网关日志通常占用最大。
  5. 限制查询范围:避免一次性扫过超长时间窗口。

7.3 一个带留存控制的 Loki 配置片段

limits_config:
  retention_period: 168h
  max_query_lookback: 168h

compactor:
  retention_enabled: true
  delete_request_store: filesystem
  working_directory: /loki/compactor

8. Kubernetes 场景下的排障实践

日志不是独立使用的,最有价值的方式是“带着问题去查”。

8.1 场景一:Pod 重启,但原因不明确

建议排查顺序:

  1. kubectl get pod -n <ns> 看状态
  2. kubectl describe pod 看 Events
  3. Loki 按 pod + container 查询启动前后日志
  4. 对照是否出现 OOM、配置缺失、依赖超时、探针失败

8.2 场景二:某个服务突然大量报错

建议先在 Loki 里按 appnamespace 定位,再缩小到具体 pod

{namespace="prod", app="payment"} |= "ERROR"

如果所有 Pod 同时报错,通常更像依赖故障;如果只有单个 Pod 报错,通常更像实例级异常、节点异常或配置漂移。

8.3 场景三:节点局部故障

如果日志只集中出现在某个 node 上,可以直接带上节点标签筛选:

{namespace="prod", app="checkout", node="worker-3"}

这类问题常常和磁盘、网络、容器运行时、节点压力有关,此时就要和 Metrics 联动一起看。

9. 本章小结

这一章可以记住 5 个结论:

  1. kubectl logs 是查看入口,不是日志平台。::cite[193]
  2. Kubernetes 做集群日志,核心思路是每个节点部署日志代理,再统一汇聚到后端。::cite[193]
  3. Loki 的优势在于“只索引标签,不索引正文”,因此更适合 Kubernetes 这种标签驱动环境。::cite[451]
  4. Loki 是否好用,关键不在于装没装,而在于 labels 设计是否克制。::cite[451]
  5. 留存策略一定要尽早做,不然日志平台的第一个问题往往不是查不到,而是太贵。::cite[464]

📝 版权声明:本文为原创技术博客,转载请注明出处。

如文章中存在错误或不准确之处,欢迎在评论区指正,感谢您的阅读与支持!

上一篇

5.1:Volume 类型:容器之外的数据如何保存

下一篇

7.3 Tracing:OpenTelemetry