Loki + Promtail 搭建 Kubernetes 日志收集方案(含完整 YAML)

Kubernetes 上做日志收集,EFK(Elasticsearch + Fluentd + Kibana)很重,存储和运维成本都高。如果你的诉求是「把各 Pod 的 stdout/stderr 集中起来、能在 Grafana 里按标签查」,Loki + Promtail 是更轻的一套:不全文索引、只索引标签,存储省、部署简单。

这篇给一套可直接 kubectl apply 的完整方案:Promtail(DaemonSet)采集 → Loki(单体)存储 → Grafana 查询。最后说清它的适用边界和大规模时该怎么演进。

文中命名空间、镜像、存储均为通用占位,替换成你自己的即可。

环境

  • Kubernetes(containerd 或 docker runtime 都可,下面会说差异)
  • Loki:grafana/loki 2.9.x,单体(monolithic)模式,boltdb-shipper + filesystem 后端
  • Promtail:grafana/promtail 2.9.x,DaemonSet
  • Grafana:已有,用来加 Loki 数据源查询
  • 命名空间统一用 logging

架构

       每个 K8s 节点
   ┌─────────────────────┐
   │ Promtail (DaemonSet) │  读 /var/log/pods/*
   │  每节点 1 个          │  按 K8s 元数据打标签(namespace/pod/app...)
   └──────────┬──────────┘
              │ HTTP push  /loki/api/v1/push
              ▼
   ┌─────────────────────┐
   │ Loki (StatefulSet)   │  单体:distributor+ingester+querier+compactor 一体
   │  Service :3100       │  boltdb-shipper(索引) + filesystem(日志块)
   └──────────┬──────────┘
              │ 持久化
              ▼
        PVC / 本地盘 /var/loki
              ▲
              │ 数据源查询(LogQL)
          Grafana

核心分工:Promtail 负责"采集 + 打标签",Loki 负责"存储 + 检索"。Loki 的省,来自它只对标签建索引、日志正文压缩成块存储,不像 ES 全文索引。

一、部署 Loki

Loki 单体模式把所有角色塞进一个进程,配置简单,适合中小日志量。三个对象:ConfigMap(配置)、StatefulSet(带持久卷)、Service。

apiVersion: v1
kind: Namespace
metadata: { name: logging }
---
apiVersion: v1
kind: ConfigMap
metadata: { name: loki-config, namespace: logging }
data:
  loki.yaml: |
    auth_enabled: false                 # 单租户,租户名默认 fake
    server:
      http_listen_port: 3100
      grpc_listen_port: 9096
    common:
      path_prefix: /var/loki
      replication_factor: 1
      ring:
        instance_addr: 127.0.0.1
        kvstore: { store: inmemory }
    schema_config:
      configs:
        - from: 2023-01-01
          store: boltdb-shipper         # 索引用 boltdb-shipper
          object_store: filesystem      # 日志块存本地文件系统
          schema: v11
          index: { prefix: index_, period: 24h }
    storage_config:
      boltdb_shipper:
        active_index_directory: /var/loki/index
        cache_location: /var/loki/cache
        cache_ttl: 24h
      filesystem:
        directory: /var/loki/chunks
    compactor:
      working_directory: /var/loki/compactor
      retention_enabled: true           # 开启基于 compactor 的过期删除
      delete_request_store: filesystem
      retention_delete_delay: 2h
    limits_config:
      retention_period: 168h            # 日志保留 7 天
      reject_old_samples: true
      reject_old_samples_max_age: 168h
      # 入口写入限流(按你的真实日志量调;默认偏小,量大务必显式设)
      ingestion_rate_mb: 16
      ingestion_burst_size_mb: 32
      per_stream_rate_limit: 8MB
      per_stream_rate_limit_burst: 16MB
    table_manager:
      retention_deletes_enabled: true
      retention_period: 168h
---
apiVersion: apps/v1
kind: StatefulSet
metadata: { name: loki, namespace: logging }
spec:
  serviceName: loki
  replicas: 1
  selector: { matchLabels: { app: loki } }
  template:
    metadata: { labels: { app: loki } }
    spec:
      containers:
        - name: loki
          image: grafana/loki:2.9.8
          args: ["-config.file=/etc/loki/loki.yaml"]
          ports: [{ containerPort: 3100, name: http }]
          volumeMounts:
            - { name: loki-config, mountPath: /etc/loki }
            - { name: loki-data,   mountPath: /var/loki }
          resources:                    # 一定要设,别裸跑:wal+内存chunk 吃内存
            requests: { cpu: "1", memory: 2Gi }
            limits:   { cpu: "4", memory: 8Gi }
      volumes:
        - name: loki-config
          configMap: { name: loki-config }
  volumeClaimTemplates:                 # 用 PVC 持久化数据
    - metadata: { name: loki-data }
      spec:
        accessModes: ["ReadWriteOnce"]
        resources: { requests: { storage: 100Gi } }
        # storageClassName: <你的 SC>
---
apiVersion: v1
kind: Service
metadata: { name: loki, namespace: logging }
spec:
  selector: { app: loki }
  ports: [{ name: http, port: 3100, targetPort: 3100 }]

几个配置要点:

  • auth_enabled: false:单租户模式,所有日志归到默认租户 fake,省去多租户配置。
  • boltdb-shipper + filesystem:索引和日志块都落本地盘(PVC)。这是单体最简形态。
  • retention_period: 168h + compactor.retention_enabled: true:日志只留 7 天,由 compactor 自动删过期数据——两个都要设,光设保留期不开 compactor 删除是不会真删的。
  • resources 必须设:Loki 的 ingester 会把未落盘的数据放内存 + WAL,裸跑遇到写入洪峰容易把节点内存吃爆。

二、部署 Promtail

Promtail 是 DaemonSet(每个节点一个),需要 RBAC 去读 Pod 元数据、需要 hostPath 挂宿主机日志目录。四个对象:ServiceAccount、ClusterRole、ClusterRoleBinding、ConfigMap、DaemonSet。

apiVersion: v1
kind: ServiceAccount
metadata: { name: promtail, namespace: logging }
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata: { name: promtail }
rules:
  - apiGroups: [""]
    resources: [nodes, services, pods]
    verbs: [get, list, watch]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata: { name: promtail }
roleRef: { apiGroup: rbac.authorization.k8s.io, kind: ClusterRole, name: promtail }
subjects:
  - { kind: ServiceAccount, 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: {}                              # 解析 containerd/CRI 日志格式
        kubernetes_sd_configs:
          - role: pod
        relabel_configs:
          # 从 K8s 元数据提取标签:app
          - source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_name, __meta_kubernetes_pod_label_app, __meta_kubernetes_pod_name]
            regex: ^;*([^;]+)(;.*)?$
            action: replace
            target_label: app
          # node_name
          - source_labels: [__meta_kubernetes_pod_node_name]
            action: replace
            target_label: node_name
          # namespace
          - source_labels: [__meta_kubernetes_namespace]
            action: replace
            target_label: namespace
          # pod
          - source_labels: [__meta_kubernetes_pod_name]
            action: replace
            target_label: pod
          # container
          - source_labels: [__meta_kubernetes_pod_container_name]
            action: replace
            target_label: container
          # 日志文件路径(关键:指向该容器的日志文件)
          - source_labels: [__meta_kubernetes_pod_uid, __meta_kubernetes_pod_container_name]
            separator: /
            replacement: /var/log/pods/*$1/*.log
            action: replace
            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
      tolerations: [{ operator: Exists }]        # 让它能上所有节点(含污点节点)
      containers:
        - name: promtail
          image: grafana/promtail:2.9.8
          args: ["-config.file=/etc/promtail/promtail.yaml"]
          env:
            - { name: HOSTNAME, valueFrom: { fieldRef: { fieldPath: spec.nodeName } } }
          volumeMounts:
            - { name: config,     mountPath: /etc/promtail }
            - { name: run,        mountPath: /run/promtail }
            - { name: pods,       mountPath: /var/log/pods, readOnly: true }
      volumes:
        - { name: config, configMap: { name: promtail-config } }
        - { name: run,  hostPath: { path: /run/promtail } }
        - { name: pods, hostPath: { path: /var/log/pods } }

要点:

  • relabel_configs 是 Promtail 的灵魂:它把 K8s 的 __meta_kubernetes_* 元数据转成 Loki 的查询标签(namespace/pod/container/app/node_name)。没有它,日志进了 Loki 也没法按服务区分。
  • __path__:最后那条 relabel 把日志文件路径拼成 /var/log/pods/*<pod_uid>/*.log,告诉 Promtail 去读哪些文件。
  • runtime 差异(重要):上面挂的是 /var/log/pods,适配 containerd(现在主流)。如果是老的 docker runtime,还要额外挂 /var/lib/docker/containers。挂错目录会一条日志都收不到。
  • positions.yaml:记录每个文件读到的偏移,Promtail 重启后接着读、不重复。

三、接入 Grafana

Grafana 加一个 Loki 类型的数据源,URL 填 Service 地址即可,无需认证(auth_enabled: false):

http://loki.logging.svc.cluster.local:3100

然后在 Explore 里用 LogQL 查,例如:

{namespace="prod", app="my-svc"} |= "error"     # 查 prod 下 my-svc 含 error 的日志
{namespace="prod"} | json | level="ERROR"        # 解析 json 后按字段过滤
sum by (app) (rate({namespace="prod"}[5m]))      # 按 app 统计日志速率

四、适用边界与演进

这套单体 + 本地盘适合中小规模(单集群、日志量不极端)。日志量大了要往两个方向走:

  • 存储换对象存储(S3/OSS/COS):多副本 Loki 不能共享本地 filesystem,分布式的前提是把 chunks/index 放对象存储。
  • 架构转 SSD(Simple Scalable Deployment):把 read/write/backend 三组拆开,write(ingester)按写入量独立扩副本——单点写入瓶颈就消失了。

另外,日志量大时,与其无脑扩容,先治理往往更划算:按级别分流(DEBUG 不入库)、高频日志采样、缩短 retention、控制标签基数(别把 request_id 这种高基数字段做成标签)。

快速参考

部署顺序

kubectl apply -f loki.yaml       # ns + cm + sts + svc
kubectl apply -f promtail.yaml   # sa + rbac + cm + ds
kubectl -n logging get pod,svc

自检

# Loki 就绪
kubectl -n logging exec deploy/grafana -- wget -qO- http://loki.logging:3100/ready
# Promtail 在每个节点都起来了
kubectl -n logging get pod -l app=promtail -o wide
# 直接查有没有数据进来(label 在 result[].stream,不是 metric)
curl -sG http://<loki>:3100/loki/api/v1/query_range \
  --data-urlencode 'query={namespace=~".+"}' \
  --data-urlencode "start=$(date -d '-5 min' +%s)000000000" \
  --data-urlencode "end=$(date +%s)000000000" --data-urlencode 'limit=20'

几条铁律

  • retention_periodcompactor.retention_enabled 要一起设,否则日志不会被真正删除。
  • Loki 必须设 resources:ingester 的 WAL + 内存 chunk 会吃内存,裸跑遇洪峰易 OOM。
  • Promtail 挂的日志目录按 runtime 选:containerd → /var/log/pods;docker → 另加 /var/lib/docker/containers
  • ingestion_rate_mb 等限流参数按真实日志量显式设,默认值偏小、量大会拒收丢日志。
  • 标签只放低基数维度(namespace/app/pod 级别),高基数字段(request_id 等)别做标签,否则 stream 爆炸、内存和 WAL 都遭殃。
  • 单体 + 本地盘是中小规模方案;要扩就上对象存储 + SSD 模式,别在单体上硬堆。
posted @ 2026-06-03 18:40  Hello_worlds  阅读(14)  评论(0)    收藏  举报