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/loki2.9.x,单体(monolithic)模式,boltdb-shipper+filesystem后端 - Promtail:
grafana/promtail2.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_period和compactor.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 模式,别在单体上硬堆。

浙公网安备 33010602011771号