Loki 日志采集落地:从单机 Helm 部署到 Promtail 采集与 Grafana 查询

Kubernetes 里看日志这件事,最常见的痛点不是"没工具",而是"工具太重、维护成本太高"。很多团队一开始用 EFK/ELK,索引能力强,但资源、运维复杂度和容量成本也跟着上来:集群一忙,日志平台先报警。

Loki 的思路更贴近运维日常:不做全文索引,只索引 标签(labels) ,日志原文压缩后存储。你用 LogQL 先按标签把范围缩小,再用 |= 之类的过滤在日志内容里做匹配。它换掉了一部分"随便搜"的自由度,换来更低的资源消耗和更容易长期跑稳。

这篇文章按我在生产里落地 Loki 的顺序来写:先把 Loki 用 Helm 在集群里跑起来,再把 Promtail 在不同场景(K8s 节点、虚拟机 nginx)接入,最后把 Grafana 查询和排障套路串起来。


1. 方案怎么选:Loki 到底省在哪

Loki 的核心取舍是:

  • 索引只做标签 :比如 namespacepodapphostjob 这类维度可控、基数不爆炸的字段
  • 内容不建索引:日志行原文不进倒排索引,存储压力和索引构建开销都小很多
  • 查询方式变了 :先用 {label="value"} 缩范围,再用 |=|~ 过滤内容

经验上,Loki 适合这些场景:

  • 你主要用"按服务/按命名空间/按主机"定位问题,然后在一个较小范围里搜索关键字
  • 你更在意平台跑得稳、运维简单、成本可控
  • 你愿意花点心思把标签体系设计好(这一步做对了,后面都省心)

不适合的场景也要提前说清楚:

  • 你强依赖"任意字段全文检索"且日志结构化程度不高
  • 你习惯把 trace_iduser_idrequest_id 这类高基数字段都当索引用

2. 组件与数据流:GLP(Grafana + Loki + Promtail)

整体链路很直白:

  1. Promtail 在节点/主机上"读日志文件"或"发现 Pod 日志路径"
  2. Promtail 把每一行日志打上标签,推送到 Loki
  3. Grafana 作为查询界面,通过 Loki API 拉取/查询日志

几个容易踩的点先放在这里:

  • 标签基数(cardinality)决定生死:标签越"多且变化快",Loki stream 越多,内存和索引压力越大
  • 路径与权限比配置更常出问题:Promtail 读不到文件、读到了但 positions 写不了、轮转后断档
  • 网络连通性要优先验证:Promtail 推送是 HTTP,连不上就是"啥都没有"

3. 前置条件清单

以常见的"集群内 Loki + 节点/主机 Promtail"组合为例,部署前建议至少满足:

  • Kubernetes 集群可用,kubectl/helm 可用
  • 有合适的持久化存储(StorageClass),本地临时盘不建议长期存日志
  • 能访问节点/Pod 日志目录(DaemonSet 通常需要 hostPath + 权限)
  • 规划好暴露方式:NodePort / Ingress / 内网访问
  • 如果用对象存储(S3/OSS/COS 等),Secret 先准备好(本文先按单机/本地 PV 讲)

4. Helm 单机模式部署 Loki(简单、够用、便于验证)

4.1 获取 Chart

bash 复制代码
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update

# 示例:拉取指定版本(按你环境固定版本,别"永远最新")
helm pull grafana/loki-stack --version 2.10.2

4.2 国内镜像与版本对齐

国内环境最常见的问题就是镜像拉不到。你的环境里如果已经有镜像加速或私有仓库,优先走统一镜像策略。

下面是一个"用国内镜像仓库替换"的示例(注意:Loki 与 Promtail 版本最好对齐同一大版本,避免奇怪的兼容问题):

bash 复制代码
# 示例镜像(按你们实际仓库改)
swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/grafana/loki:2.6.1
swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/grafana/promtail:2.9.3

我更推荐把这些写进 values.yaml,而不是堆一长串 --set。但如果你想快速验证,--set 也够用。

4.3 安装/升级

下面沿用"快速落地"的方式,启用持久化并用 NodePort 暴露 Loki:

bash 复制代码
helm upgrade --install loki /root/loki-stack -n vke-system \
  --set loki.image.repository=swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/grafana/loki \
  --set loki.service.type=NodePort \
  --set loki.service.nodePort=31167 \
  --set loki.persistence.enabled=true \
  --set loki.persistence.storageClassName=nfs-csi \
  --set loki.persistence.size=100Gi \
  --set promtail.enabled=true

落地经验:

  • 单机模式适合先跑通链路、验证采集和查询;真正上生产要尽快做 存储后端、限流与保留策略 的规划
  • NodePort 很方便,但别忘了 安全组/防火墙;Promtail 在别的网段推不上来时,Grafana 里就是"空"

5. Grafana 接入 Loki(把链路闭环)

Grafana 里接入 Loki,常见两种方式:

  • 你自己已有 Grafana:在 Data sources 里新增 Loki
  • Chart 里启用 Grafana:直接用内置 Grafana(适合验证,不建议长期当生产主 Grafana)

5.1 Data Source 配置要点

在 Grafana 里新增 Loki 数据源时,最常用的只需要:

  • URL:http://<loki-service>:3100http://<node-ip>:<nodeport>
  • Save & Test 通过即可

5.2 LogQL 查询习惯(先按标签,再筛内容)

几个我排障时常用的查询:

logql 复制代码
{job="nginx"}
logql 复制代码
{namespace="prod", app="api"} |= "error"
logql 复制代码
{pod=~"api-.*"} |~ "timeout|context deadline exceeded"

查询不出日志时,先别急着怀疑 Loki:

  • 把时间范围拉大一点(Promtail 断过一段就很明显)
  • 先用 {job="xxx"} 这种最宽的条件看看有没有任何流
  • 再逐步加标签与过滤条件

6. Promtail 部署:两类场景(K8s 节点与虚拟机)

Promtail 本质就是"读文件 + 打标签 + 推送"。部署形态主要两种:

  • 集群里:DaemonSet(每个节点一个)
  • 集群外:二进制/容器方式跑在虚拟机或物理机

6.1 虚拟机 nginx 采集:最小可用配置

下面这个配置和很多 nginx 场景一致:读 nginx 日志目录,打上 job/host 标签,推送到 Loki。

/etc/promtail-config.yaml 示例:

yaml 复制代码
server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://xxx.xxx.xxx.xxx:31167/loki/api/v1/push

scrape_configs:
  - job_name: gateway-nginx
    static_configs:
      - targets:
          - localhost
        labels:
          job: gateway-nginx
          host: xxx.xxx.xxx.xxx
          __path__: /vecps/nginx/common/nginx/logs/*.log

启动方式(临时验证):

bash 复制代码
nohup /bin/promtail-linux-amd64 -config.file=/etc/promtail-config.yaml 1>&2 > /var/log/promtail.log &

生产里我通常会把它做成 systemd 服务,原因很现实:机器重启、进程被 OOM 杀掉、日志目录权限变化,这些都需要"自动拉起 + 有日志"。
创建专用用户(可选但推荐)

bash 复制代码
sudo useradd --system --no-create-home --shell /sbin/nologin promtail
sudo chown promtail:promtail /var/log/promtail.log
sudo chmod 644 /etc/promtail-config.yaml

编写服务

bash 复制代码
sudo tee /etc/systemd/system/promtail.service << 'EOF'
[Unit]
Description=Promtail service for shipping logs to Loki
Documentation=https://grafana.com/docs/loki/latest/send-data/promtail/
After=network.target

[Service]
Type=simple
User=promtail
Group=promtail
ExecStart=/bin/promtail-linux-amd64 -config.file=/etc/promtail-config.yaml
Restart=on-failure
RestartSec=5
StandardOutput=append:/var/log/promtail.log
StandardError=append:/var/log/promtail.log

# 安全加固(可选)
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target
EOF
bash 复制代码
# 重载 systemd 配置
sudo systemctl daemon-reload

# 启动服务
sudo systemctl start promtail

# 设置开机自启
sudo systemctl enable promtail

# 查看状态
sudo systemctl status promtail

这里也有几个常见坑:

  • positions.yaml 写不了:目录没权限或磁盘满,会导致重启后重复采集或断档
  • 日志轮转后丢日志:__path__ 写得太死、轮转策略不一致
  • job/host 标签不统一:Grafana 查询时非常痛苦

6.2 K8s 采集:DaemonSet + Kubernetes 服务发现(推荐)

Kubernetes 环境下,Promtail 更推荐用 kubernetes_sd_configs 做服务发现,配合 relabel 把 namespace/pod/container/node 这些标签带上来。这样你在 Grafana 里按命名空间、按 Pod 查日志会顺手很多。

如果你要采集 容器 stdout/stderr,大部分 chart 默认就能跑通。

如果你要采集 容器内文件日志,要把"容器内路径"和"宿主机路径"打通(本质是 hostPath 共享目录),然后让 Promtail 知道该读哪个文件。

工作原理:

  1. 应用 Pod: 通过 hostPath 将宿主机上的一个目录挂载到容器内,应用将日志写入这个目录。同时,在该 Pod 的 metadata.annotations 中声明日志文件的路径。
  2. Promtail DaemonSet: 配置 kubernetes_sd_configs 来发现 Pod,并通过 relabel_configs 读取 Pod 的 annotation。如果 annotation 存在,就将其值作为 path ,告诉 Promtail 去宿主机的对应路径下抓取日志。
    Promtail 配置示例 (relabel_configs):这个配置段的作用是:找到所有带有 loki.grafana.com/scrape: "true" 这个 annotation 的 Pod,并读取 loki.grafana.com/path 的值作为日志文件路径。

一个更好用的模式是用 annotation 控制哪些 Pod 需要采集文件日志:
编辑 Promtail ConfigMap,增加如下的配置

在Promtail 侧示例(重点是 __meta_kubernetes_pod_annotation_loki_io_logfile -> __path__):

yaml 复制代码
server:
  log_level: info
  log_format: logfmt
  http_listen_port: 3101
  

clients:
  - url: http://loki:3100/loki/api/v1/push

positions:
  filename: /run/promtail/positions.yaml

scrape_configs:
  # See also https://github.com/grafana/loki/blob/master/production/ksonnet/promtail/scrape_config.libsonnet for reference
  - job_name: kubernetes-pods
    pipeline_stages:
      - cri: {}
    kubernetes_sd_configs:
      - role: pod
    relabel_configs:
      - source_labels:
          - __meta_kubernetes_pod_controller_name
        regex: ([0-9a-z-.]+?)(-[0-9a-f]{8,10})?
        action: replace
        target_label: __tmp_controller_name
      - source_labels:
          - __meta_kubernetes_pod_label_app_kubernetes_io_name
          - __meta_kubernetes_pod_label_app
          - __tmp_controller_name
          - __meta_kubernetes_pod_name
        regex: ^;*([^;]+)(;.*)?$
        action: replace
        target_label: app
      - source_labels:
          - __meta_kubernetes_pod_label_app_kubernetes_io_instance
          - __meta_kubernetes_pod_label_instance
        regex: ^;*([^;]+)(;.*)?$
        action: replace
        target_label: instance
      - source_labels:
          - __meta_kubernetes_pod_label_app_kubernetes_io_component
          - __meta_kubernetes_pod_label_component
        regex: ^;*([^;]+)(;.*)?$
        action: replace
        target_label: component
      - action: replace
        source_labels:
        - __meta_kubernetes_pod_node_name
        target_label: node_name
      - action: replace
        source_labels:
        - __meta_kubernetes_namespace
        target_label: namespace
      - action: replace
        replacement: $1
        separator: /
        source_labels:
        - namespace
        - app
        target_label: job
      - action: replace
        source_labels:
        - __meta_kubernetes_pod_name
        target_label: pod
      - action: replace
        source_labels:
        - __meta_kubernetes_pod_container_name
        target_label: container
      - action: replace
        replacement: /var/log/pods/*$1/*.log
        separator: /
        source_labels:
        - __meta_kubernetes_pod_uid
        - __meta_kubernetes_pod_container_name
        target_label: __path__
      - action: replace
        regex: true/(.*)
        replacement: /var/log/pods/*$1/*.log
        separator: /
        source_labels:
        - __meta_kubernetes_pod_annotationpresent_kubernetes_io_config_hash
        - __meta_kubernetes_pod_annotation_kubernetes_io_config_hash
        - __meta_kubernetes_pod_container_name
        target_label: __path__
		
      # === 新增:支持 loki.io/logfile 注解 ===
      - source_labels: [__meta_kubernetes_pod_annotation_loki_io_logfile]
        action: keep
        regex: .+
      - source_labels: [__meta_kubernetes_pod_annotation_loki_io_logfile]
        target_label: __path__
      # ===================================
  

limits_config:
  

tracing:
  enabled: false

应用 Pod 示例(用 annotation 指定宿主机上真实的日志文件路径):

yaml 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: busybox-log-test
  annotations:
    loki.io/logfile: '/mnt/log/busybox/*.log'
spec:
  replicas: 2
  selector:
    matchLabels:
      app: busybox-log
  template:
    metadata:
      labels:
        app: busybox-log
      annotations:
        loki.io/logfile: '/mnt/log/busybox/*.log'
    spec:
      volumes:
        - name: log
          hostPath:
            path: /mnt/log/busybox
            type: DirectoryOrCreate
      containers:
        - name: busybox
          image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/busybox:latest
          command: ["/bin/sh"]
          args: ["-c", "while true; do echo \"$(date): This is a log entry from busybox\" >> /var/log/busybox/busybox.log; sleep 5; done"]
          volumeMounts:
            - mountPath: /var/log/busybox
              name: log
          resources:
            requests:
              memory: "64Mi"
              cpu: "50m"
            limits:
              memory: "128Mi"
              cpu: "100m"

这个模式我用下来最省心的点在于:

  • 默认不采集文件日志,避免"全量把宿主机目录扫一遍"
  • 哪些 Pod 需要采集、采集哪些文件,由应用自己声明,运维侧只管规则

7. 标签体系怎么定:别把 Loki 用成 ES

Loki 的体验好不好,80% 看标签体系。一个比较稳的经验组合是:

  • K8s 相关:clusternamespaceapppodcontainernode
  • 机器相关:jobhost
  • 环境相关:env(prod/staging/dev)

尽量别做这些事:

  • trace_id/request_id/user_id 当标签(基数爆炸,stream 爆炸)
  • 把完整路径/URL 当标签(同样是基数炸弹)
  • 标签命名不统一(同一个概念 host/node/instance 混着来)

如果你确实要按 trace_id 查,建议把它当内容过滤:

logql 复制代码
{namespace="prod", app="api"} |= "trace_id=abcd1234"

8. 排障套路:从"链路"开始查

8.1 Promtail 推送失败

先在 Promtail 机器上确认 Loki 可达:

bash 复制代码
curl -sS http://xxx.xxx.xxx.xxx:31167/ready

再看 Promtail 自己的日志(你用 systemd 就 journalctl -u promtail -f,用 nohup 就看文件)。

常见原因:

  • URL 写错、NodePort 不通、安全组没放行
  • Loki 端限流或拒绝(通常能在 Loki 日志里看到)
  • 证书/代理配置导致请求出不去

8.2 Grafana 里"查不到日志"

按这个顺序走,基本不会绕圈:

  • 用最宽条件 {job="xxx"}{namespace="xxx"} 看有没有任何流
  • 把时间范围拉到"最近 6h/24h"
  • 再慢慢加过滤条件,定位是"没采到"还是"过滤太严"

8.3 Loki 内存突然涨、查询变慢

这通常不是"机器小",而是标签基数失控:

  • 最近是不是加了新的标签(尤其是从日志内容里提取出来的)
  • pod 是否频繁变化(短生命周期任务很多)
  • 有没有把 URL、path、request_id 这种东西做成标签

解决思路就是"收敛标签维度",并给 Loki 设置合理的 limits。

相关推荐
码农小卡拉1 天前
Prometheus 监控 SpringBoot 应用完整教程
spring boot·后端·grafana·prometheus
咖啡啡不加糖2 天前
Grafana 监控服务指标使用指南:打造可视化监控体系
java·后端·grafana
世界尽头与你2 天前
(修复方案)CVE-2021-43798: Grafana路径遍历漏洞
安全·grafana
qq_312920115 天前
Proxmox VE 监控:把集群指标秒级推送到 InfluxDB 2.x,Grafana 大屏一步到位
运维·grafana
A-刘晨阳6 天前
Prometheus + Grafana + Alertmanager 实现邮件监控告警及配置告警信息
运维·云计算·grafana·prometheus·监控·邮件
电话交换机IPPBX-3CX7 天前
如何使用 Grafana 可视化你的 3CX 呼叫中心电话系统
grafana·ip pbx·电话交换机·企业电话系统
Otto_10278 天前
在 OpenStack Rocky 中部署 Prometheus + Grafana
openstack·grafana·prometheus
小北方城市网8 天前
Spring Boot Actuator+Prometheus+Grafana 生产级监控体系搭建
java·spring boot·python·rabbitmq·java-rabbitmq·grafana·prometheus
牛奶咖啡139 天前
Prometheus+Grafana构建云原生分布式监控系统(九)_pushgateway的使用
云原生·grafana·prometheus·pushgateway·pushgateway使用场景·推数据到pushgateway·pushgateway的使用