多台服务器日志分散难查?用Promtail+Loki+Grafana搭建集中检索平台

线上服务一旦拆到多台机器,日志查询就会变成一件很烦的事。我维护的几个后端服务分布在四台服务器上,平时排查问题基本靠 ssh 登上去,再 tail -f 或者 grep。单机还好,一旦某个请求跨了多个服务,或者要对比几台机器同一时间段的报错,就得开好几个终端来回切,效率低到让人怀疑人生。

之前也考虑过 ELK,但 Elasticsearch 对内存要求比较高,我的机器上还要跑业务进程,不太想为了查日志再吃掉几个 G 内存。后来选了 Loki 这套方案,它的思路和 ES 不一样:只对标签建立索引,日志正文压缩后直接存对象存储或本地磁盘,资源占用小很多。配合 Promtail 采集、Grafana 查询,整套跑起来内存占用比我预期低。

这篇文章记录一下我从零搭这套平台的过程,包括配置、标签设计、查询语法,以及跑起来之后真实遇到过的问题。

为什么是 Loki 而不是 ELK

先说选型。这不是说 Loki 比 ELK 好,而是场景不同。

方案 优点 缺点 适用场景
ELK(Elasticsearch + Logstash + Kibana) 全文检索强,聚合分析能力完整,生态成熟 资源占用高,ES 对内存和磁盘 IO 要求大,运维成本高 日志量大、需要复杂全文检索和聚合分析的团队
Loki + Promtail + Grafana 资源占用低,和 Prometheus/Grafana 体系天然契合,部署简单 不支持全文索引,查询依赖标签过滤,聚合能力弱于 ES 中小规模、已有 Grafana、主要靠标签定位问题的场景
直接 ssh + grep 零部署成本 多机、跨服务、历史回溯场景基本不可用 单机、临时排查

我这边的情况是:服务数量不算多,日志量每天大概几个 G,查询诉求主要是"某台机器某个服务在某段时间的报错",而不是"全文搜索某个关键词出现在哪些日志里"。这种以标签定位为主的查询,正好是 Loki 的强项。

【关键结论】如果你的查询主要是按服务、主机、时间范围过滤,Loki 性价比很高;如果经常要做全文关键词检索和复杂聚合,ES 更合适。

组件分工

三个组件各管一段:

  1. Promtail:部署在每台被采集的机器上,负责读本地日志文件,打上标签,推送到 Loki。
  2. Loki:接收日志,按标签建索引,把日志内容压缩存储。
  3. Grafana:连到 Loki 作为数据源,提供查询界面和仪表盘。

数据流向大致是:日志文件 → Promtail(采集+打标签)→ Loki(索引+存储)→ Grafana(查询展示)。

部署方式

我用的是 docker-compose,把 Loki 和 Grafana 放在一台机器上(下面叫它日志服务器),Promtail 单独部署在每台业务机上。这样业务机只需要跑一个很轻的 Promtail 容器。

先看 Loki 和 Grafana 这一侧。我用的版本是 Loki 2.9.x 和 Grafana 10.x,这两个版本当前比较稳定,配置写法也是现在主流的。

yaml 复制代码
# docker-compose.yml  ------ 部署在日志服务器
version: "3.8"

services:
  loki:
    image: grafana/loki:2.9.6
    container_name: loki
    ports:
      - "3100:3100"
    volumes:
      - ./loki-config.yml:/etc/loki/local-config.yaml
      - ./loki-data:/loki
    command: -config.file=/etc/loki/local-config.yaml
    restart: unless-stopped

  grafana:
    image: grafana/grafana:10.4.2
    container_name: grafana
    ports:
      - "3000:3000"
    volumes:
      - ./grafana-data:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=change_me
    depends_on:
      - loki
    restart: unless-stopped

Loki 的配置文件我用了接近单机默认的写法,主要确认存储路径和监听端口:

yaml 复制代码
# loki-config.yml
auth_enabled: false

server:
  http_listen_port: 3100

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

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

limits_config:
  reject_old_samples: true
  reject_old_samples_max_age: 168h

这里有几个点值得说一下。auth_enabled: false 是关掉多租户,单机自己用没必要开。schema: v13 是当前 Loki 推荐的 schema 版本,和 tsdb 存储配合。reject_old_samples_max_age: 168h 是拒绝超过 7 天的旧日志,防止客户端时钟不对导致乱序写入。

【注意】schema_config 里的 from 日期一旦定下来,后续如果要改 schema,需要新增一条配置项而不是改这一条,Loki 是按时间段选择 schema 的。这一点官方文档有说明,改错了会导致旧数据读不出来。

filesystem 存储适合单机或者挂载了共享存储的场景。如果日志量再大、想上对象存储(比如 S3、MinIO),把 object_store 换成对应类型即可,这个我没实际配过,就不展开了。

Promtail 采集配置

业务机上的 Promtail 配置是重点,标签怎么打直接决定了后面查询好不好用。

yaml 复制代码
# promtail-config.yml ------ 部署在每台业务机
server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://<日志服务器IP>:3100/loki/api/v1/push

scrape_configs:
  - job_name: app-logs
    static_configs:
      - targets:
          - localhost
        labels:
          job: app
          host: server-01
          __path__: /var/log/myapp/*.log

positions.yaml 记录每个文件读到哪一行了,Promtail 重启后从断点继续,不会重复推送。这个文件要保证可写。

clients 指向 Loki 的 push 接口。

scrape_configs 里,__path__ 是匹配日志文件的路径,labels 里定义的标签会附加到这批日志上。上面这个配置的意思是:采集 /var/log/myapp/ 下所有 .log 文件,给它们打上 job=app、host=server-01 两个标签。

每台机器上的配置只有 host 不一样,其他都一样。实际部署时我用了一个模板,部署脚本里替换 host 值。

标签设计的坑

这里是我踩得比较实的一个问题。一开始我想当然地把一些动态字段也做成了标签,比如把日志里的 request_id、user_id 提取出来当标签。结果 Loki 很快就报警说标签基数太高,查询也变得很慢。

原因是 Loki 的索引是基于标签组合的。标签的唯一组合数量 (基数)一旦爆炸,索引就撑不住了。request_id 这种几乎每条都不一样的值,做成标签等于给每条日志建一个索引,完全违背了 Loki 的设计初衷。

【踩坑提醒】标签只放基数低、可枚举 的维度,比如 host、job、env、level。像 request_id、user_id、trace_id 这种高基数字段,要么放在日志正文里用查询语法过滤,要么用 Loki 的 structured metadata(需要较新版本,且行为有差异,我没有深入验证)。

调整之后,我的标签就固定在 host、job、env 这几个上:

yaml 复制代码
labels:
  job: app
  host: server-01
  env: prod

这样标签组合数量就是"机器数 × 服务数 × 环境数",非常可控。

日志格式建议

Loki 查询时如果日志是结构化 JSON,过滤会方便很多。我这边后端服务用的是 Python,日志输出改成了 JSON 格式,方便后续按字段过滤。

python 复制代码
# logging_config.py
import json
import logging
import sys


class JsonFormatter(logging.Formatter):
    def format(self, record: logging.LogRecord) -> str:
        payload = {
            "ts": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
        }
        # 如果有额外字段(比如 request_id),一并带上
        if hasattr(record, "request_id"):
            payload["request_id"] = record.request_id
        if record.exc_info:
            payload["exc"] = self.formatException(record.exc_info)
        return json.dumps(payload, ensure_ascii=False)


def get_logger(name: str) -> logging.Logger:
    logger = logging.getLogger(name)
    logger.setLevel(logging.INFO)
    handler = logging.StreamHandler(sys.stdout)
    handler.setFormatter(JsonFormatter())
    logger.addHandler(handler)
    return logger

这样每条日志就是一行 JSON。注意 request_id 是放在正文里的,不是标签,查询时用 Loki 的 JSON 解析语法过滤。

在 Grafana 里查日志

Grafana 加 Loki 数据源很简单:Configuration → Data Sources → Add data source → 选 Loki,URL 填 http://loki:3100(如果 Grafana 和 Loki 在同一个 compose 网络里)或者日志服务器 IP。

配好之后,在 Explore 页面就能查了。Loki 的查询分两部分:标签选择器 和过滤器。

标签选择器必须至少有一个非空的匹配条件,比如:

logql 复制代码
{host="server-01", job="app"}

这会查出这台机器上 app 服务的所有日志。然后可以加过滤器:

logql 复制代码
{host="server-01", job="app"} |= "ERROR"

|= 是包含某个字符串,!= 是不包含,|~ 是正则匹配。这些都是对日志正文做过滤,配合标签先缩小范围,效率会好很多。

如果日志是 JSON,可以用 | json 解析后按字段过滤:

logql 复制代码
{host="server-01", job="app"} | json | level="ERROR"

还可以按 request_id 查一条请求的完整链路(即使它跨了多个服务):

logql 复制代码
{job="app"} | json | request_id="abc-123"

这比挨个 ssh 上去 grep 快太多了。

常用查询整理

几个我平时用得比较多的写法:

  1. 查某台机器最近的报错:

    logql 复制代码
    {host="server-01", job="app"} |= "ERROR"
  2. 查某个服务所有机器上的 500 错误:

    logql 复制代码
    {job="app"} | json | status="500"
  3. 统计每分钟错误日志条数(用于做告警):

    logql 复制代码
    sum(count_over_time({job="app"} |= "ERROR" [1m]))
  4. 排除健康检查这类噪音:

    logql 复制代码
    {job="app"} != "healthcheck"

第 3 条这种是 metric 查询,可以直接配成 Grafana 的告警规则。

实际遇到的问题

标签基数报警

前面提过,一开始把 request_id 做成标签,Loki 日志里出现 max label cardinality 类似的告警,查询也明显变慢。把高基数字段从标签里去掉之后恢复正常。这是 Loki 使用中最容易犯的错,没有之一。

时间范围查询慢

Loki 查询性能和时间范围强相关。查最近 15 分钟很快,查最近 7 天就会慢,因为它要扫过整个时间段的数据。我的做法是:日常排查控制在几小时内,需要长期回溯的场景单独处理,而不是一上来就查一周。

limits_config 里的 max_query_length 可以限制单次查询的最大时间跨度,我设了一个相对保守的值,防止有人误查超大范围把 Loki 拖垮。这个参数具体取值要结合自己的数据量和机器配置,我没有一个通用数字。

磁盘增长

日志是持续写入的,磁盘会一直涨。Loki 本身有 retention 配置,但我用的是 filesystem 存储,实际清理行为需要结合 compactor 相关配置一起看,这块我配得比较简单(靠 reject_old_samples_max_age 控制写入,定期手动清理旧 chunks)。如果对保留策略有严格要求,建议用对象存储配合 retention 配置,或者接一个定时清理脚本。这一点我没有做完整的自动化验证,属于待完善的部分。

是否值得搭

回到最开始的问题。这套方案适合什么样的场景?

  • 服务分布在多台机器,ssh 逐台查已经明显影响效率;
  • 查询以标签+时间范围为主,不是重度全文检索;
  • 机器资源有限,不想为日志系统投入太多内存;
  • 已经在用 Grafana,希望日志和指标在同一个界面看。

如果这几点都符合,Loki 这套组合的性价比很高。如果日志量特别大、或者需要 ES 那种全文检索和复杂聚合,那就老老实实上 ELK,别硬套。

我搭完之后,最直观的变化是排查一个跨服务问题的时间从"开四个终端来回切"变成了"在 Grafana 里一条查询搞定"。尤其是按 request_id 追链路,之前基本靠手动拼,现在直接查。

后续我打算再补两块:一是把错误日志的 metric 查询配上 Grafana 告警,出问题主动通知而不是等人来查;二是把 retention 策略做成自动化的,省得手动清磁盘。这两块落地之后,这套平台才算真正完整。

相关推荐
ysu_03141 小时前
uv实战指南-从Python版本虚拟环境到pyproject依赖管理
开发语言·python·pip·uv·虚拟环境·包管理·依赖管理
2601_962203511 小时前
需要本地或私有化部署,又担心硬件和配置成本?快鹭KuWork、OpenOcta、安捷AI、AnythingLLM四款企业级AI智能体办公平台技术对比
人工智能
蜗牛互联网1 小时前
OLMo-core 3的token gerrymandering提醒:MoE路由要按时间窗验收
java·人工智能·wpf
欢喜躲在眉梢里1 小时前
时序数据库选型指南:从大数据架构视角拆解 Apache IoTDB 的适用边界
大数据·人工智能·ai·架构·时序数据库·模型
勤劳X码农1 小时前
2026年抖音AI配音软件怎么选?
人工智能
打不了嗝 ᥬ᭄1 小时前
AI-Agent入门
人工智能·agent
Dawson Zhu1 小时前
Cookbook Agent:拓扑Codebook方法与多Agent通信效率优化
人工智能·语言模型·架构·aigc·agi
打工仔折腾 AI1 小时前
DiPlay 实测:iPhone 绕过硬件盒子直连 BYD 车机的思路拆解
android·人工智能·后端·python·gradle·iphone·ai agent 实战
二川bro1 小时前
AI测试Agent 25个全套Skill,直接搭建完整自动化测试流水线
人工智能