日志散在几台机器上?用一台机器搭起 Promtail + Loki + Grafana 日志中心

凌晨的告警群里,最常见的一句话是:接口超时了,谁看一下日志。下面用一个脱敏后的典型场景说明。某个订单接口在晚高峰变慢,它的日志分散在三台应用机的 /data/app/logs 里。排查的人只能逐台登录,用 grep 拼时间线,各机器的时间格式还不完全一致,对齐时间花的功夫比看日志还多。

这类问题不需要一套庞大的日志平台。一台 4 核 8G、带独立数据盘的 Linux 机器,就能把采集、存储和查询跑起来,支撑小团队的日常排查。本文的组合是:Promtail 在应用机上读取日志并推送,Loki 负责存储和按标签检索,Grafana 负责查询界面。三个组件都交给 systemd 托管,Loki 只监听本机地址。最后给出磁盘规划、保留期和回滚方法。

先看资源,再定保留期

日志量决定磁盘规模,这一步不能跳过。在目标机器上执行只读检查:

cat /etc/os-release
[ec2-user@ip-172-31-41-69 ~]$ cat /etc/os-release 
NAME="Red Hat Enterprise Linux"
VERSION="10.2 (Coughlan)"
RELEASE_TYPE=stable
ID="rhel"
ID_LIKE="centos fedora"
VERSION_ID="10.2"
PLATFORM_ID="platform:el10"
PRETTY_NAME="Red Hat Enterprise Linux 10.2 (Coughlan)"
ANSI_COLOR="0;31"
LOGO="fedora-logo-icon"
CPE_NAME="cpe:/o:redhat:enterprise_linux:10.2"
HOME_URL="https://www.redhat.com/"
VENDOR_NAME="Red Hat"
VENDOR_URL="https://www.redhat.com/"
DOCUMENTATION_URL="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/10"
BUG_REPORT_URL="https://issues.redhat.com/"

REDHAT_BUGZILLA_PRODUCT="Red Hat Enterprise Linux 10"
REDHAT_BUGZILLA_PRODUCT_VERSION=10.2
REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux"
REDHAT_SUPPORT_PRODUCT_VERSION="10.2"

nproc
free -h
df -h /var/lib
image-20261102220302874

预期:内存至少 4G,/var/lib 所在分区剩余空间不少于 50G。空间不足时,把下文的 retention_period 设为 72h,并在上线一周后根据实际增长量决定是否扩容。

安装二进制与运行用户

以下以 Loki 3.x 为例,版本号请在 GitHub release 页面确认后替换 LOKI_VER。下载地址:https://github.com/grafana/loki/releases/

LOKI_VER=3.7.9
cd /tmp
wget https://github.com/grafana/loki/releases/download/v${LOKI_VER}/loki-linux-amd64.zip
wget https://github.com/grafana/loki/releases/download/v2.9.5/promtail-linux-amd64.zip
sha256sum loki-linux-amd64.zip promtail-linux-amd64.zip

把 sha256sum 的结果与同一 release 页面中 SHA256SUMS 文件的校验值逐一比对。不一致就删除文件重新下载,不要继续安装。校验通过后安装:

unzip -o loki-linux-amd64.zip
unzip -o promtail-linux-amd64.zip
sudo install -m 0755 loki-linux-amd64 /usr/local/bin/loki
sudo install -m 0755 promtail-linux-amd64 /usr/local/bin/promtail
loki --version
promtail --version
id loki >/dev/null 2>&1 || sudo useradd --system --no-create-home --shell /usr/sbin/nologin loki
sudo mkdir -p /etc/loki /var/lib/loki/chunks /var/lib/loki/rules /var/lib/loki/compactor /etc/promtail /var/lib/promtail
sudo chown -R loki:loki /var/lib/loki
image-20261102221554930

Promtail 需要读取 /var/log 和应用日志目录,因此它以 root 运行。positions 文件目录要保持可写,否则重启后会丢失读取位置。

Loki 配置:单机要关掉的依赖与要打开的保留

单机配置的重点有三处。存储用 filesystem,ring 用 inmemory,不依赖对象存储和外部 KV。schema 使用 tsdb 与 v13,这是 Loki 3.x 推荐的组合。compactor 必须开启 retention_enabled,否则 retention_period 不会真正删除旧数据。把下面内容保存为 /etc/loki/config.yml:

auth_enabled: false

server:
  http_listen_address: 127.0.0.1
  http_listen_port: 3100
  grpc_listen_port: 9095

common:
  path_prefix: /var/lib/loki
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory
  storage:
    filesystem:
      chunks_directory: /var/lib/loki/chunks
      rules_directory: /var/lib/loki/rules

schema_config:
  configs:
    - from: 2024-04-01
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h

compactor:
  working_directory: /var/lib/loki/compactor
  retention_enabled: true
  delete_request_store: filesystem

limits_config:
  retention_period: 168h

analytics:
  reporting_enabled: false

schema_config 里的 from 是 schema 的生效日期。写入过数据之后,不要修改已有条目的 from 或 schema,而是新增一条生效日期更晚的配置。改掉旧条目会让 Loki 找不到已有数据的索引。

校验配置并确认属主:

sudo chown root:loki /etc/loki/config.yml && sudo chmod 0640 /etc/loki/config.yml
loki -verify-config -config.file=/etc/loki/config.yml
echo $?

预期:退出码为 0,没有 error 输出。如果安装的版本不支持 -verify-config,先用 loki -help 确认参数,再决定是否直接启动并看日志。

image-20261102222033877

Promtail 采集:先用一个 job 验证链路

Promtail 配置只做三件事:指定 positions 文件,推送到本机 Loki,定义采集路径和标签。把下面内容保存为 /etc/promtail/config.yml,host 改成本机主机名:

server:
  http_listen_address: 127.0.0.1
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /var/lib/promtail/positions.yaml

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

scrape_configs:
  - job_name: syslog
    static_configs:
      - targets: [localhost]
        labels:
          job: syslog
          host: web-01
          __path__: /var/log/syslog

  - job_name: order-app
    static_configs:
      - targets: [localhost]
        labels:
          job: order-app
          host: web-01
          __path__: /data/app/logs/*.log

通配符路径在加入前先执行 ls /data/app/logs/*.log,确认没有把含密钥或用户隐私的文件一起采进来。标签只放 job、host、环境这类基数低的值,不要放 request_id、用户 ID 或订单号。每个唯一的标签组合都会形成一条独立的日志流,标签基数过高会拖慢写入和查询。

用 systemd 托管三个服务

Loki 的单元文件保存为 /etc/systemd/system/loki.service:

[Unit]
Description=Loki log aggregation
After=network-online.target

[Service]
User=loki
Group=loki
ExecStart=/usr/local/bin/loki -config.file=/etc/loki/config.yml
Restart=on-failure
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

Promtail 的单元文件保存为 /etc/systemd/system/promtail.service,并依赖 loki.service:

[Unit]
Description=Promtail log shipper
After=network-online.target loki.service

[Service]
User=root
ExecStart=/usr/local/bin/promtail -config.file=/etc/promtail/config.yml
Restart=on-failure

[Install]
WantedBy=multi-user.target

启动并设置开机自启:

sudo systemctl daemon-reload
sudo systemctl enable --now loki promtail
systemctl status loki promtail --no-pager

预期:两个服务都是 active (running)。如果 Loki 启动失败,先看 journalctl -u loki -n 100 --no-pager,不要反复 restart 掩盖报错。

image-20261102222650264
image-20261102222748903

Grafana 接入与链路验证

按 Grafana 官方文档安装 OSS 版,然后把数据源写成文件,避免在界面里手工点配置后无人记得。保存为 /etc/grafana/provisioning/datasources/loki.yaml:

apiVersion: 1
datasources:
  - name: Loki
    type: loki
    access: proxy
    url: http://127.0.0.1:3100
    isDefault: true
    jsonData:
      maxLines: 5000

启动 Grafana,并只放行办公网段访问 3000 端口。下面的网段是示例,按你的实际网络替换:

sudo systemctl enable --now grafana-server

首次登录后立即修改 admin 密码。验证分三层,逐层通过再往下走。

第一层,Loki 就绪。刚启动的前十几秒返回未就绪属于正常现象,等待后再测:

curl -s http://127.0.0.1:3100/ready

预期输出 ready。

第二层,Promtail 有没有在推送。连续执行两次,数值应当增长:

curl -s http://127.0.0.1:9080/metrics | grep '^promtail_sent_entries_total'
image-20261102224920969

第三层,写入一条带唯一标记的日志,再从 Loki 查回来:

logger -t loki-smoke "smoke-$(date +%s)"
sleep 15
curl -G -s http://127.0.0.1:3100/loki/api/v1/query_range \
  --data-urlencode 'query={job="syslog"} |= "loki-smoke"' \
  --data-urlencode 'limit=5'

预期:返回的 JSON 中 status 为 success,data.result 非空,values 里能看到刚写入的标记。如果 promtail 指标在涨、查询却为空,先检查 job 标签是否与配置一致,再看 Promtail 的 positions 是否指向了旧位置。

接着在 Grafana 的 Explore 中选择 Loki,执行下面的查询。它查的是应用日志里的超时记录:

{job="order-app", host="web-01"} |= "timeout"
image-20261102225719052

按 host 统计每分钟超时次数,适合放进仪表盘或告警规则:

sum by (host) (count_over_time({job="order-app"} |= "timeout" [5m]))

磁盘、保留期与回滚

上线后第二天,记录一次数据目录大小,用两次的差值估算日增长量:

du -sh /var/lib/loki
df -h /var/lib/loki

估算方法很简单:日增长量乘以 retention_period 的天数,结果应小于可用空间的 70%。超过时,先缩短保留期,再考虑扩盘。磁盘使用率超过 80% 就要告警,因为写满后日志会丢失,而且通常在最需要它的时候发生。

修改 retention_period 后,compactor 会在后续周期删除过期数据。它不会恢复已经删除的数据,改之前请确认是否还需要那段历史。

变更前备份配置:

sudo cp -a /etc/loki /etc/loki.bak.$(date +%F)
sudo cp -a /etc/promtail /etc/promtail.bak.$(date +%F)

如果新版本或新配置出问题,回滚顺序如下。先停止采集,再停止存储,恢复旧二进制和旧配置,最后启动:

sudo systemctl stop promtail loki
sudo install -m 0755 /usr/local/bin/loki.<旧版本> /usr/local/bin/loki
sudo cp -a /etc/loki.bak.<日期>/config.yml /etc/loki/config.yml
sudo systemctl start loki promtail

回滚时不要删除 /var/lib/loki。那里是全部历史日志,删除后无法找回。Loki 停止期间 Promtail 会重试,但缓冲有限,长时间中断可能丢日志,所以计划性变更应选在业务低峰,并提前通知。

案例证据链:超时到底出在哪一层

下面用一个脱敏后的典型场景说明排查过程,时间、延迟和次数均为示例数据。

21:40 起,订单接口 P99 从约 300ms 升到 2 秒左右。最先怀疑数据库,但数据库慢查询日志没有明显增加,排除了这条线。

接着按 host 统计 timeout 次数:

sum by (host) (count_over_time({job="order-app"} |= "timeout" [5m]))

结果显示 web-01 和 web-02 的计数同时上升,web-03 基本为零。三台机器共用同一个数据库,因此问题更可能在应用本身或它调用的下游。

再查同一时间段的具体日志:

{job="order-app"} |= "pool exhausted"

日志集中出现支付网关连接池耗尽的记录。对比当天的配置变更,发现前一天的发布把该连接池的上限从 50 改成了 5。这个配置在两台机器上生效,第三台没有发布,解释了 web-03 的正常表现。

临时止损是回滚这一个配置项,并在 10 分钟内观察到超时计数回落。根治是把连接池参数纳入发布前校验,并为“等待连接耗时”增加指标。防复发则是对 pool exhausted 关键字设置 5 分钟计数告警。

这个案例的证据链是:时间窗口与 host 维度的对比、日志中的直接错误信息、配置变更记录、回滚后的指标回落。四者互相印证,才把结论定在配置上,而不是“数据库可能慢”这类猜测上。

收藏清单

  • 不要把 Loki 的 3100 端口暴露到公网。它默认没有鉴权,本文配置只监听 127.0.0.1。原因是查询路径一旦被未授权访问,应用日志就可能外泄。
  • 不要把 request_id、用户 ID、订单号做成标签。每个不同取值都会生成新的日志流,基数失控会直接拖慢写入和查询。
  • 务必先执行配置校验再重启服务。配置写错时服务起不来,日志链路会静默中断,而很多团队要到出事时才发现。
  • 不要修改已有 schema_config 条目的 from 和 schema。Loki 依靠它定位历史数据的索引,改动后旧数据会查不到。
  • 务必监控数据盘使用率并设置 80% 告警。磁盘写满后日志会丢失,而且通常在最需要它的时候发生。
  • 采集通配符路径前先 ls 确认。应用目录里常混有含凭证或个人信息的文件,采进来后会在 Grafana 里被所有能登录的人看到。
  • 不要在排障时直接删除 /var/lib/loki。历史数据删除后无法恢复,应优先通过缩短保留期让它自然过期。
  • 确认 /var/lib/promtail 可写。positions 文件丢失后,Promtail 可能从头重读日志,造成重复数据。
  • 每次变更前保存配置备份,并记录旧版本二进制的位置。回滚依赖这两样东西,事后补救通常来不及。

结论与下一步

单机 Promtail + Loki + Grafana 的价值在于,它用很少的机器和配置,把“逐台登录找日志”变成“按标签查时间线”。关键不在组件多,而在于每一层都有验证:Loki 就绪、Promtail 在推送、查询能查回标记日志。

今晚先在一台测试机上跑通 smoke 查询,确认链路无误后,再把第一台业务机接入。觉得这份方案有用,欢迎点赞、在看,也转发给身边正在维护日志系统的同事。

公众号:耕云躬行录

个人博客:躬行笔记

参与讨论