本文是 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 个:
- 只适合临时查看:你得知道 Pod 名称,还要手工进入具体命名空间、具体容器。::cite[418]
- 保留能力很弱:Kubernetes 文档指出,
kubectl logs只会返回最新日志文件的内容;如果容器轮转、Pod 被驱逐,原始日志并不会天然长期保留。::cite[193] - 不适合跨 Pod、跨节点聚合:当副本很多时,你很难用
kubectl logs高效比对多个实例的共同异常。::cite[193] - 不适合检索与分析:你无法像日志平台那样按照
namespace、app、pod、错误关键字或时间范围做聚合查询。
换句话说,kubectl logs 很适合“快速看一下某个 Pod 的输出”,但不适合承担留存、聚合、检索、统计、审计、回放这些日志平台职责。
2. Kubernetes 中容器日志的真实路径
理解 Loki 之前,先把 Kubernetes 原生日志路径理顺:
- 应用把日志写到
stdout/stderr - 容器运行时按 CRI 格式写入节点本地日志文件
- kubelet 管理日志轮转与访问
kubectl logs通过 Kubernetes API 拉取单个容器日志- 节点级日志采集器再把这些日志统一送入外部日志系统
Kubernetes 官方把这种做法称为 cluster-level logging:也就是在每个节点上运行一个日志代理,统一把容器日志送到后端存储。::cite[193]
3. Loki 与 Promtail 的基本架构
Grafana Loki 官方文档把 Loki 定义为一个高可用、可横向扩展、多租户的日志聚合系统。它与传统日志系统最大的不同是:Loki 不索引日志正文,只索引日志流的标签(labels)元数据,因此在 Kubernetes 这类标签丰富、实例众多的环境里,通常能以更低成本完成日志检索。::cite[451]
Loki 的典型数据流可以理解成下面 3 段:
- Promtail / Agent:运行在节点侧,负责读取容器日志文件、解析、加标签、转发
- Loki:负责接收日志流、索引标签、存储日志块、处理查询
- 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 最常用的检索有两步:
- 先按标签选中日志流
- 再按关键字、正则或解析后的字段做过滤
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 检索日志时的推荐顺序
实战里,建议按这个顺序缩小范围:
- 先选
namespace - 再选
app - 再选
pod或container - 最后再按关键字过滤
这样查询会更快,也更容易避免把无关日志一起扫进来。
7. 留存策略与成本控制
Loki 官方文档指出,日志留存由 Compactor 负责;如果没有开启 compactor.retention-enabled,日志默认会一直保留。文档还指出,可以按 tenant 或 stream 维度定义更细粒度的留存策略。::cite[464]
这意味着:如果你不主动设计日志留存,日志成本只会越来越高。
7.1 留存设计建议
可以按日志价值分层:
| 日志类型 | 建议保留时长 | 说明 |
|---|---|---|
| 核心生产错误日志 | 30~90 天 | 便于事故回溯与审计 |
| 普通应用日志 | 7~30 天 | 满足日常排障即可 |
| 调试级日志 | 1~7 天 | 仅在问题排查期短暂开启 |
| 访问日志全文 | 3~14 天 | 高流量场景尤其要控制 |
7.2 成本控制的几个抓手
- 降低高基数标签:这是 Loki 成本优化里最关键的一步。::cite[451]
- 减少无意义 Debug 日志:别把“开发期日志级别”原封不动搬进生产。
- 分环境留存:生产、预发、测试的保留时长不应该一样。
- 热点日志单独治理:访问日志、审计日志、网关日志通常占用最大。
- 限制查询范围:避免一次性扫过超长时间窗口。
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 重启,但原因不明确
建议排查顺序:
kubectl get pod -n <ns>看状态kubectl describe pod看 Events- Loki 按
pod+container查询启动前后日志 - 对照是否出现 OOM、配置缺失、依赖超时、探针失败
8.2 场景二:某个服务突然大量报错
建议先在 Loki 里按 app 和 namespace 定位,再缩小到具体 pod:
{namespace="prod", app="payment"} |= "ERROR"
如果所有 Pod 同时报错,通常更像依赖故障;如果只有单个 Pod 报错,通常更像实例级异常、节点异常或配置漂移。
8.3 场景三:节点局部故障
如果日志只集中出现在某个 node 上,可以直接带上节点标签筛选:
{namespace="prod", app="checkout", node="worker-3"}
这类问题常常和磁盘、网络、容器运行时、节点压力有关,此时就要和 Metrics 联动一起看。
9. 本章小结
这一章可以记住 5 个结论:
kubectl logs是查看入口,不是日志平台。::cite[193]- Kubernetes 做集群日志,核心思路是每个节点部署日志代理,再统一汇聚到后端。::cite[193]
- Loki 的优势在于“只索引标签,不索引正文”,因此更适合 Kubernetes 这种标签驱动环境。::cite[451]
- Loki 是否好用,关键不在于装没装,而在于 labels 设计是否克制。::cite[451]
- 留存策略一定要尽早做,不然日志平台的第一个问题往往不是查不到,而是太贵。::cite[464]
📝 版权声明:本文为原创技术博客,转载请注明出处。
如文章中存在错误或不准确之处,欢迎在评论区指正,感谢您的阅读与支持!