跳转至

日志与 ELK / Loki

容器日志默认写在节点磁盘上,Pod 一删就散。要把它变成可检索、可告警的数据,需要一条从采集、传输、存储到查询的链路。本章用 Loki 和 ECK 两套方案走同一条链路,最后给出选型依据。

难度 · 进阶 预计 65 分钟 前置:Pod 与 Service、Helm 基础

你将学到

  • 理解 stdout/CRI 日志文件、kubectl logs 与节点 DaemonSet 采集三者的关系。
  • 画出 Loki 的 distributor、ingester、querier 读写路径,说明 MinIO 各 bucket 的作用。
  • 按 minio → loki → promtail → grafana 的顺序部署 Loki Stack,用 LogQL 查询日志。
  • 用 ECK 部署 Elasticsearch、Kibana、Filebeat,获取密码并验证索引。

为什么需要它

指标告诉你“错误率涨了”,日志才告诉你“为什么”。但 Kubernetes 的日志有三个麻烦:容器是临时的,Pod 重建后旧日志随节点文件一起被清理;日志分散在每个节点,不可能逐台 SSH;业务希望按 namespace、Pod、容器、关键字组合检索,手工 grep 无法满足。

因此日志系统必须解决采集、传输与存储、检索三件事。Loki 用“只索引标签、不索引内容”换取低成本,ELK 用倒排索引换取强检索能力,两者没有绝对优劣,只有场景匹配。指标侧见 Prometheus 监控,资源与弹性见 调度、资源与弹性。

核心概念

Kubernetes 的日志采集模型

容器运行时把 stdout/stderr 写成 CRI 日志文件,路径形如 /var/log/pods/<namespace>_<pod>_<uid>/<container>/0.log;/var/log/containers/ 下还有带 Pod、命名空间、容器名的符号链接,方便采集器用通配符发现。kubectl logs 由 kubelet 读取这些文件,不经过 API Server 的日志存储,所以 Pod 删除后日志通常就没了。

生产做法是在每个节点跑一个 DaemonSet 采集器,用 hostPath 挂载 /var/log/pods 和 /var/log/containers,通过 Kubernetes SD 发现 Pod 元数据,给日志打上 namespace、pod、container、node 标签后发往后端。kubelet 默认按 containerLogMaxSize=10Mi、containerLogMaxFiles=5 轮转,采集器必须能处理文件被截断或重命名。

Loki 的读写路径

写入路径:Promtail 把日志推到 loki-gateway,gateway 转发给 distributor;distributor 校验租户和标签,按标签哈希分发给多个 ingester,并按 replication_factor 写副本;ingester 在内存里压成 chunk、写 WAL,达到阈值后 flush 到对象存储。读取路径:querier 从 ingester 取未 flush 的数据,从对象存储取历史 chunk,按时间范围合并、去重后返回。

Loki 只索引标签,不索引正文。{namespace="default", pod="demoapp-xxx"} 能快速定位 chunk,但 |= "OutOfMemory" 是在 chunk 内逐行扫描。这让存储成本远低于 ELK,代价是全文检索和大范围模糊查询更慢。

flowchart LR
  PT[Promtail DaemonSet] --> GW[loki-gateway]
  GW --> D[distributor]
  D --> I[ingester]
  I --> S[(MinIO S3)]
  Q[querier] --> S
  G[Grafana] --> GW

图:Promtail 写入经 distributor 到 ingester,ingester flush 到 MinIO;querier 同时读 ingester 和 MinIO。

MinIO 里通常建三个 bucket:chunks 存压缩日志块,ruler 存告警规则,admin 存管理数据。Loki 访问 MinIO 必须开 s3ForcePathStyle: true,因为 MinIO 使用路径风格寻址;没有 TLS 时还要 insecure: true。Loki 有单体、简单可扩展和微服务三种模式,实验使用简单可扩展模式。

Promtail 与部署顺序

Promtail 以 DaemonSet 运行,配置分三段:clients 指定网关和租户 ID,scrape_configs 用 kubernetes_sd_configs 发现 Pod,pipeline_stages 做 CRI 解析、JSON 解析和标签提取。cri: {} 去掉 CRI 日志头,json 把字段提成临时标签,labels 再把需要的字段固化成 Loki 标签。不要把请求 ID、用户 ID 这类高基数字段做成标签。

部署顺序由依赖决定:MinIO Operator → Tenant 与 bucket → Loki → Promtail → Grafana,每一步都为下一步提供依赖。

Loki 启动时就要连对象存储并校验 bucket,所以 MinIO 和 bucket 必须先就绪;Promtail 启动时就要推日志,所以 Loki gateway 必须先可用;Grafana 只是查询方,放最后。仓库 Loki-Stack/kubernetes/ 正是按这个顺序组织的。

LogQL 与 ECK

LogQL 分两类:日志查询以 {label="value"} 开头,后接 |=、|~、!=、!~ 过滤,再做 json、logfmt、regexp、pattern 解析;指标查询把日志流变成数值,用 rate、count_over_time、bytes_over_time 等函数,再配合 sum by、topk 聚合。

目的 LogQL
包含关键字 {namespace="default"} \|= "error"
正则匹配 {namespace="default"} \|~ "timeout\|refused"
JSON 字段过滤 {namespace="default"} \| json \| status >= 500
每秒日志行数 sum by (namespace) (rate({namespace="default"}[5m]))

ECK 是 Elastic 官方 Operator,用 CRD 管理 Elasticsearch、Kibana、Beat、Agent、Logstash;Elasticsearch/Kibana 的 API 是 v1,Beat 是 v1beta1,在 v1.31 上都是当前版本。Elasticsearch 的 HTTP 入口是 <name>-es-http,自动生成的 elastic 密码在 <name>-es-elastic-user Secret 的 elastic 键;Kibana 入口是 <name>-kb-http。Filebeat 的 Beat CR 以 DaemonSet 运行,通过 autodiscover 读取容器日志,写入 data stream,索引名形如 .ds-filebeat-8.14.0-YYYY.MM.DD-000001。

仓库有两处版本不一致:eck-operator/README.md 写 Elasticsearch 8.11.3、20Gi、nfs-csi,YAML 却是 8.14.0、5Gi、openebs-hostpath;Filebeat 的 README 写 8.11.3,YAML 是 8.14.0。以 YAML 为准。

维度 Loki ELK
索引方式 只索引标签,正文压缩 倒排索引全文检索
存储成本 低 高
查询能力 标签定位 + 行过滤 强全文检索与聚合
运维复杂度 较低 较高,需管分片与 ILM

动手实验

实验:按顺序部署 Loki Stack 并部署 ECK

目标:用 MinIO 做对象存储,按顺序部署 Loki、Promtail、Grafana 并查询日志;再用 ECK 部署 Elasticsearch、Kibana、Filebeat,验证索引。

步骤 1 · 部署 MinIO Operator 与 Tenant

helm repo add minio-operator https://operator.min.io
helm upgrade --install operator minio-operator/operator \
  -n minio-operator --create-namespace
minio-tenant-values.yaml
secrets: {name: myminio-env-configuration, accessKey: minioadmin, secretKey: MageEdu123}
tenant:
  name: minio
  configuration: {name: myminio-env-configuration}
  pools:
  - {name: pool-0, servers: 1, volumesPerServer: 2, storageClassName: openebs-hostpath, size: 5Gi}
  certificate: {requestAutoCert: false}
  buckets:
  - name: chunks
  - name: ruler
  - name: admin
helm upgrade --install --namespace minio --values minio-tenant-values.yaml \
  --create-namespace minio minio-operator/tenant

步骤 2 · 部署 Loki

loki-values.yaml
deploymentMode: SimpleScalable
loki:
  replication_factor: 1
  storage:
    type: s3
    bucketNames: {chunks: chunks, ruler: ruler, admin: admin}
    s3:
      endpoint: minio.minio.svc.cluster.local:9000
      accessKeyId: minioadmin
      secretAccessKey: MageEdu123
      s3ForcePathStyle: true
      insecure: true
  schemaConfig:
    configs:
    - from: 2024-04-01
      store: tsdb
      object_store: s3
      schema: v13
      index: {prefix: index_, period: 24h}
write: {replicas: 1}
read: {replicas: 1}
backend: {replicas: 1}
helm repo add grafana https://grafana.github.io/helm-charts
helm upgrade --install --values loki-values.yaml loki grafana/loki \
  -n loki --create-namespace

原 values 的 s3.endpoint 没写端口,MinIO S3 API 在 9000,必须补上;实验把副本降到 1,生产建议至少 3。

步骤 3 · 部署 Promtail

helm upgrade --install promtail grafana/promtail -n loki \
  --set 'config.clients[0].url=http://loki-gateway/loki/api/v1/push' \
  --set 'config.clients[0].tenant_id=tenant1'

步骤 4 · 部署 Grafana 并查询

grafana-values.yaml
adminUser: admin
adminPassword: magedu.com
datasources:
  datasources.yaml:
    apiVersion: 1
    datasources:
    - name: loki
      type: loki
      url: http://loki-gateway
      jsonData: {httpHeaderName1: X-Scope-OrgID}
      secureJsonData: {httpHeaderValue1: tenant1}
helm upgrade --install grafana grafana/grafana -n loki -f grafana-values.yaml
kubectl -n loki port-forward svc/loki-gateway 3100:80 &
curl -s -H 'X-Scope-OrgID: tenant1' \
  'http://127.0.0.1:3100/loki/api/v1/query_range?query=%7Bnamespace%3D%22default%22%7D&limit=2' \
  | jq -r '.data.result[].values[][1]'
10.244.1.5 - - [07/Nov/2026:10:00:01 +0000] "GET / HTTP/1.1" 200
10.244.2.7 - - [07/Nov/2026:10:00:02 +0000] "GET /healthz HTTP/1.1" 200

Grafana 用 kubectl -n loki port-forward svc/grafana 3000:80 打开,账号 admin/magedu.com;401 no org id 表示请求漏了 X-Scope-OrgID。

步骤 5 · 部署 ECK 与 Elasticsearch

helm repo add elastic https://helm.elastic.co
helm install elastic-operator elastic/eck-operator \
  -n elastic-system --create-namespace
kubectl apply -f https://raw.githubusercontent.com/iKubernetes/learning-k8s/master/eck-operator/elasticsearch-myes-cluster.yaml
kubectl -n elastic-system get elasticsearch
PASSWORD=$(kubectl -n elastic-system get secret myes-es-elastic-user \
  -o go-template='{{.data.elastic | base64decode}}')
kubectl -n elastic-system port-forward svc/myes-es-http 9200:9200 &
curl -sk -u "elastic:$PASSWORD" https://127.0.0.1:9200 | jq -r '.version.number'

kubectl -n elastic-system get elasticsearch 应显示 HEALTH=green、VERSION=8.14.0,curl 返回 8.14.0。仓库 YAML 用 3 个节点,实验环境按资源情况调整;版本以 YAML 的 8.14.0 为准。

步骤 6 · 部署 Kibana 与 Filebeat,查看索引

kubectl apply -f https://raw.githubusercontent.com/iKubernetes/learning-k8s/master/eck-operator/kibana-myes.yaml
kubectl apply -f https://raw.githubusercontent.com/iKubernetes/learning-k8s/master/eck-operator/beats-filebeat.yaml
curl -sk -u "elastic:$PASSWORD" 'https://127.0.0.1:9200/_cat/indices/.ds-filebeat-*?v'

输出中应出现 .ds-filebeat-8.14.0-YYYY.MM.DD-000001 索引。Filebeat 以 DaemonSet 采集节点日志,elasticsearchRef 指向 myes,kibanaRef 指向 kibana;Kibana 会自动加载 filebeat-* pattern。清单会丢弃 kube-system、logging、elastic-system 等命名空间,别在那里找应用日志。

验证清单

  • MinIO Tenant 就绪,chunks、ruler、admin 三个 bucket 存在。
  • Loki 的 read/write/backend Pod 均 Running,gateway Service 可访问。
  • Promtail DaemonSet 的 DESIRED 等于节点数,Pod 全部 Ready。
  • 带 X-Scope-OrgID: tenant1 的 LogQL 查询能返回日志行。
  • ECK 的 Elasticsearch 为 green,myes-es-elastic-user 能解出密码。

常见问题与排错

现象 可能原因 处理方式
Promtail 报 401 no org id 请求没带租户头 clients 加 tenant_id,Grafana 数据源加 X-Scope-OrgID
Loki 报 x509: certificate signed by unknown authority MinIO 开了 TLS,Loki 却按 HTTP 连 关闭 requestAutoCert,或给 Loki 配正确 CA
Loki 报 NoSuchBucket bucket 没创建,或 endpoint 少了端口 先建 chunks/ruler/admin,endpoint 写 :9000
Promtail CrashLoop,提示 positions positions 目录不可写或 hostPath 权限不对 挂载可写的 /run/promtail,检查 securityContext
Elasticsearch Pod Pending StorageClass 不存在或容量不足 kubectl describe pvc 看事件,换可用 StorageClass
Filebeat 没有生成索引 kibanaRef 指向不存在的 Kibana,或 RBAC 不足 先部署 Kibana,确认 Beat Pod 日志无连接错误

进阶挑战

  1. 给 Promtail 增加 json 与 labels 阶段,把 level 和 status 提成标签,比较查询性能与基数变化。
  2. 用 Loki Ruler 配置“5 分钟内 error 超过 100 行”的告警,并接入 Alertmanager。

小结

  • 容器日志落在节点文件里,kubectl logs 只是 kubelet 的读取接口,生产必须用 DaemonSet 采集。
  • Loki 只索引标签,写入经 distributor/ingester,读取由 querier 合并 ingester 与对象存储的数据。
  • MinIO 提供 S3 兼容后端,bucket 要在 Loki 启动前创建,endpoint 要带 :9000 并开启 path-style。
  • Loki 胜在成本和运维简单,ELK 胜在全文检索和聚合,选型看查询模式而不是流行度。

延伸阅读