日志与 ELK / Loki¶
容器日志默认写在节点磁盘上,Pod 一删就散。要把它变成可检索、可告警的数据,需要一条从采集、传输、存储到查询的链路。本章用 Loki 和 ECK 两套方案走同一条链路,最后给出选型依据。
你将学到¶
- 理解 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
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
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 并查询
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 日志无连接错误 |
进阶挑战¶
- 给 Promtail 增加
json与labels阶段,把level和status提成标签,比较查询性能与基数变化。 - 用 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 胜在全文检索和聚合,选型看查询模式而不是流行度。