在VPS上使用Grafana Loki实现集中式日志管理

4 分钟阅读·Matthieu·monitoringlogqlloggingdocker-composepromtaillokigrafana|

通过Docker Compose在单台VPS上部署Grafana Loki、Promtail和Grafana。收集systemd、Docker和Nginx日志,使用LogQL查询,并配置日志保留策略。

分散在/var/logdocker logs输出中的日志,一旦你运行超过两个服务就变得难以管理。本教程使用Docker Compose在单台VPS上部署Grafana + Loki + Promtail技术栈。你将收集systemd、Docker容器和Nginx的日志,使用LogQL查询它们,配置保留策略防止日志填满磁盘,并为面向公网的服务器加固整个技术栈。

完成后,Loki的HTTP API将可用于程序化查询。在VPS上使用Ollama进行AI日志分析:用本地LLM检测异常文章在此基础上将日志输入本地LLM进行异常检测。

Grafana + Loki + Promtail技术栈的作用是什么?

Grafana Loki是一个开源日志聚合系统,它只索引日志标签(元数据),不索引日志条目的完整文本。这使得它的资源消耗远低于Elasticsearch。Loki将压缩的日志块存储在文件系统或对象存储中。搭配Promtail进行收集、Grafana进行可视化,形成一个完整的集中式日志技术栈。

三个组件各有分工:

组件 作用 资源占用
Loki 接收、存储和索引日志。处理查询请求。 空闲时300-600 MB RAM,高负载查询时最高1 GB
Promtail 发现日志源,持续读取文件,将条目发送到Loki 50-100 MB RAM
Grafana 通过Explore界面查询和可视化日志的Web UI 200-300 MB RAM

技术栈总占用:1-1.5 GB RAM。2 GB VPS是最低要求。4 GB VPS为大数据集上的LogQL查询提供充足的余量。

Loki与Elasticsearch对比: Elasticsearch索引每行日志中的每个词,提供全文搜索但消耗10-20倍的RAM和磁盘。Loki的标签索引意味着你先按标签过滤,再遍历匹配的块。对于大多数VPS工作负载,这是正确的权衡。如果你需要在TB级日志上进行全文搜索,Loki不是合适的工具。

前提条件

  • 至少2 GB RAM的VPS(推荐4 GB)。配备4 vCPU和8 GB RAM的Virtua Cloud VPS可以轻松运行此技术栈。
  • 已安装Docker和Docker Compose。如需安装帮助,请参阅[-> docker-compose-multi-service-vps]。
  • 具有sudo权限的非root用户。
  • 基本的终端操作和YAML语法知识。

验证Docker正在运行:

docker --version
docker compose version

你应该看到Docker 24+和Compose v2+。如果任一命令失败,说明Docker未安装或缺少Compose插件。

如何在VPS上使用Docker Compose部署Loki?

创建一个项目目录和三个配置文件:docker-compose.ymlloki-config.ymlpromtail-config.yml。Docker Compose文件将所有镜像固定到特定版本,设置资源限制,配置持久化卷,并将Loki绑定到localhost。

项目结构

mkdir -p ~/loki-stack/{loki-data,promtail-data}
cd ~/loki-stack

loki-data目录存放chunks、索引和预写日志(WAL)。promtail-data目录存储Promtail的位置文件,以便重启后继续读取。

设置Loki数据目录的所有权。Loki 3.x的Docker镜像以UID 10001运行,而非root。没有这一步,Loki会在创建子目录时因"permission denied"而启动失败:

chown 10001:10001 ~/loki-stack/loki-data

docker-compose.yml

services:
  loki:
    image: grafana/loki:3.6.7
    command: -config.file=/etc/loki/config.yml
    volumes:
      - ./loki-config.yml:/etc/loki/config.yml:ro
      - ./loki-data:/loki
    ports:
      - "127.0.0.1:3100:3100"
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 1g
    networks:
      - loki-net

  promtail:
    image: grafana/promtail:3.6.7
    command: -config.file=/etc/promtail/config.yml
    volumes:
      - ./promtail-config.yml:/etc/promtail/config.yml:ro
      - ./promtail-data:/var/lib/promtail
      - /var/log:/var/log:ro
      - /var/log/journal:/var/log/journal:ro
      - /run/log/journal:/run/log/journal:ro
      - /etc/machine-id:/etc/machine-id:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 256m
    depends_on:
      - loki
    networks:
      - loki-net

  grafana:
    image: grafana/grafana:11.5.2
    volumes:
      - grafana-data:/var/lib/grafana
    ports:
      - "127.0.0.1:3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD__FILE=/run/secrets/grafana_admin_pw
      - GF_SERVER_ROOT_URL=http://localhost:3000
      - GF_USERS_ALLOW_SIGN_UP=false
      - GF_ANALYTICS_REPORTING_ENABLED=false
    secrets:
      - grafana_admin_pw
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 512m
    depends_on:
      - loki
    networks:
      - loki-net

secrets:
  grafana_admin_pw:
    file: ./secrets/grafana_admin_pw

volumes:
  grafana-data:

networks:
  loki-net:
    driver: bridge

注意Loki和Grafana绑定的是127.0.0.1,而非0.0.0.0。这阻止了外部访问。你将通过SSH隧道或反向代理访问Grafana。将Loki或Grafana直接暴露到互联网是一个常见错误,大多数在线教程都存在这个问题。

restart: unless-stopped策略确保每个服务在重启后自动恢复。如果你手动执行docker compose stop停止服务,它会保持停止状态。否则会自动重启。

固定的镜像标签(3.6.711.5.2)防止意外升级。永远不要在生产环境中使用:latest。当你要升级时,修改标签然后运行docker compose up -d拉取新镜像。

生成Grafana管理员密码

永远不要使用默认凭据。生成一个强密码并将其存储在权限受限的密钥文件中:

mkdir -p ~/loki-stack/secrets
openssl rand -base64 32 > ~/loki-stack/secrets/grafana_admin_pw
chmod 644 ~/loki-stack/secrets/grafana_admin_pw

验证权限:

ls -la ~/loki-stack/secrets/grafana_admin_pw

你应该看到-rw-r--r--。文件需要所有人可读,因为Docker Compose(非Swarm模式)使用源文件权限挂载基于文件的secrets。Grafana在容器内以UID 472运行,需要读取权限。文件仍然受到其在专用secrets目录中位置的保护,只有主机的root用户才能修改它。环境变量GF_SECURITY_ADMIN_PASSWORD__FILE告诉Grafana在启动时从此文件读取密码,而不是将其嵌入compose文件中。

loki-config.yml

此配置使用TSDB和schema v13(Loki 3.x默认值)、单节点部署的文件系统存储,以及用于日志保留的compactor:

auth_enabled: false

server:
  http_listen_port: 3100
  http_listen_address: 0.0.0.0
  log_level: warn
  http_server_read_timeout: 30s
  http_server_write_timeout: 30s

common:
  ring:
    instance_addr: 127.0.0.1
    kvstore:
      store: inmemory
  replication_factor: 1
  path_prefix: /loki

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

storage_config:
  filesystem:
    directory: /loki/chunks
  tsdb_shipper:
    active_index_directory: /loki/tsdb-index
    cache_location: /loki/tsdb-cache

compactor:
  working_directory: /loki/compactor
  compaction_interval: 10m
  retention_enabled: true
  retention_delete_delay: 2h
  delete_request_store: filesystem

limits_config:
  retention_period: 720h
  max_streams_per_user: 10000
  ingestion_rate_mb: 16
  ingestion_burst_size_mb: 32
  max_label_names_per_series: 15

ingester:
  chunk_encoding: snappy
  wal:
    dir: /loki/wal
    enabled: true

chunk_store_config:
  chunk_cache_config:
    embedded_cache:
      enabled: true
      max_size_mb: 100

此配置中的关键决策:

  • auth_enabled: false 在这里是安全的,因为Loki只监听localhost(Docker内部网络 + 127.0.0.1端口绑定)。多租户配置需要auth_enabled: true并在每个请求中附带X-Scope-OrgID头。
  • retention_period: 720h 保留日志30天。如果不设置此值,Loki 3.x默认为0s(永久保留)。你的磁盘会被填满。
  • schema: v13 配合store: tsdb是Loki 3.x功能所必需的。来自Loki 2.x教程的旧boltdb-shipper配置将无法启动或产生弃用警告。
  • chunk_encoding: snappy 使用Snappy压缩chunks。比gzip快,文件略大。对于CPU比磁盘更受限的单节点来说是好的默认选择。
  • WAL已启用: 预写日志(Write-Ahead Log)在Loki写入过程中崩溃时保护数据不丢失。重启时,Loki会重放WAL以恢复未提交的条目。你会在启动日志中看到"WAL replay"消息。这是正常的。
  • max_label_names_per_series: 15 与Loki 3.x默认值一致。保持标签基数低。像user_idrequest_id这样的标签会创建太多stream并降低性能。

如何配置Promtail收集systemd journal日志?

Promtail从多个来源抓取日志并发送到Loki。以下配置在独立的scrape_configs任务中从三个来源收集:systemd journal、Docker容器和Nginx日志文件。

Promtail生命周期结束通知: Promtail已于2026年3月2日到达生命周期终点。Grafana Alloy是官方继任者。本教程使用Promtail是因为配置概念可以直接迁移到Alloy,而且数百万部署仍在使用它。请参阅下方"Promtail是否已弃用?"章节了解迁移步骤。

promtail-config.yml

server:
  http_listen_port: 9080
  log_level: warn

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

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

scrape_configs:
  # --- Systemd journal ---
  - job_name: journal
    journal:
      max_age: 12h
      labels:
        job: systemd-journal
    relabel_configs:
      - source_labels: ['__journal__systemd_unit']
        target_label: unit
      - source_labels: ['__journal_priority_keyword']
        target_label: severity
      - source_labels: ['__journal__hostname']
        target_label: hostname

  # --- Docker containers ---
  - job_name: docker
    docker_sd_configs:
      - host: unix:///var/run/docker.sock
        refresh_interval: 5s
    relabel_configs:
      - source_labels: ['__meta_docker_container_name']
        regex: '/(.+)'
        target_label: container
      - source_labels: ['__meta_docker_container_log_stream']
        target_label: stream
      - source_labels: ['__meta_docker_container_label_com_docker_compose_service']
        target_label: compose_service

  # --- Nginx access and error logs ---
  - job_name: nginx
    static_configs:
      - targets:
          - localhost
        labels:
          job: nginx
          type: access
          __path__: /var/log/nginx/access.log
      - targets:
          - localhost
        labels:
          job: nginx
          type: error
          __path__: /var/log/nginx/error.log

positions.yaml文件跟踪Promtail在每个日志源中的读取进度。如果Promtail重启,它会从上次停止的位置继续,而不是重新发送旧日志或遗漏新日志。

journal任务的工作原理

journal块通过journal API直接读取systemd journal。max_age: 12h设置告诉Promtail在首次启动时只摄入最近12小时的journal条目。没有这个设置,Promtail会尝试摄入整个journal历史记录,在运行时间较长的服务器上可能有数GB。

relabel_configs从journal条目中提取元数据作为Loki标签。__journal__systemd_unit变成unit标签(如sshd.servicenginx.service)。__journal_priority_keyword变成severity(如warningerrinfo)。这些标签让你在LogQL中高效过滤日志,无需逐行扫描。

要使journal抓取正常工作,Promtail容器需要两个卷挂载:/run/log/journal(或者如果你的系统持久化日志则是/var/log/journal)和/etc/machine-id。machine ID用于标识要读取的journal。

Docker镜像限制: 标准grafana/promtail Docker镜像未编译systemd journal支持。如果你在日志中看到support for reading the systemd journal is not compiled into this build of promtail,journal抓取将无法在Docker容器中工作。你有两个选择:

  1. 将Promtail作为主机二进制文件安装,而非使用Docker镜像。从Loki releases页面下载,直接在主机上运行,这样它可以原生访问journal API。
  2. 使用Grafana Alloy(参见下方迁移章节),其Docker镜像支持journal抓取。

Docker和Nginx文件抓取任务在Docker镜像中正常工作。只有journal抓取需要主机二进制文件或Alloy。

如何使用Promtail收集Docker容器日志?

docker任务使用Docker服务发现自动发现主机上所有运行中的容器。Promtail通过/var/run/docker.sock连接Docker守护进程,每5秒查询新容器。当容器启动或停止时,Promtail自动开始或停止跟踪其日志。

relabel_configs提取有用的元数据:

  • __meta_docker_container_name变成container标签。正则表达式'/(.+)'去掉Docker添加到容器名称前的/
  • __meta_docker_container_log_stream变成stream标签(stdoutstderr)。
  • __meta_docker_container_label_com_docker_compose_service提取Compose服务名(如lokigrafana)。此标签仅存在于Docker Compose管理的容器上。

你不需要配置每个容器。所有使用写入文件系统的日志驱动(默认的json-file驱动)的容器都会被自动发现。如果你运行了数据库容器、Web应用和缓存,三者都会自动出现在Loki中,以各自的容器名称标识。

要将特定容器排除在日志收集之外,添加一个Docker标签并过滤它:

    relabel_configs:
      # ... existing relabel rules ...
      - source_labels: ['__meta_docker_container_label_logging']
        regex: 'disabled'
        action: drop

然后在要排除的容器上:

  noisy-service:
    image: some/image
    labels:
      logging: "disabled"

如何使用Promtail抓取Nginx访问日志和错误日志?

nginx任务使用static_configs配合__path__标签来跟踪特定日志文件。与Docker服务发现不同,这需要你提前知道日志文件路径。Nginx默认写入/var/log/nginx/access.log/var/log/nginx/error.log

type标签区分访问日志和错误日志。这让你可以在LogQL中分别查询:

{job="nginx", type="access"}    # 仅访问日志
{job="nginx", type="error"}     # 仅错误日志
{job="nginx"}                   # 两者兼有

如果主机上未安装Nginx,Promtail会记录一个关于文件缺失的警告但继续抓取其他来源。这是无害的。如果你不使用Nginx,从配置中移除nginx任务即可。

对于在Docker容器中运行的Nginx,你有两个选择。可以使用Docker服务发现(容器的stdout/stderr会自动捕获)。或者将Nginx日志目录作为共享卷挂载并使用静态文件抓取器。Docker方式更简单。文件方式让你获得独立的type: accesstype: error标签。

启动技术栈

cd ~/loki-stack
docker compose up -d

验证三个容器都在运行:

docker compose ps

你应该看到lokipromtailgrafana的状态都是Up。如果任何服务显示Restarting,检查其日志:

docker compose logs <service-name> --tail=30

检查Loki是否就绪:

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

预期输出:ready。如果得到Ingester not ready: waiting for 15s after being ready,等待15秒后重试。Loki需要时间初始化ingester ring。

检查Promtail的目标:

docker compose logs promtail --tail=20

查找显示已发现目标的行。你应该看到journal、Docker socket和Nginx日志路径的条目。不应出现level=error的行。

如何在Grafana Explore中验证日志已出现?

打开SSH隧道从本地机器访问Grafana。我们使用隧道是因为Grafana在VPS上绑定到localhost,未暴露到互联网。

ssh -L 3000:127.0.0.1:3000 user@your-vps-ip

在浏览器中打开http://localhost:3000。使用用户名admin~/loki-stack/secrets/grafana_admin_pw中的密码登录。读取密码:

cat ~/loki-stack/secrets/grafana_admin_pw

添加Loki为数据源

  1. 进入Connections > Data Sources > Add data source
  2. 选择Loki
  3. 将URL设置为http://loki:3100(这是Docker内部主机名,不是localhost)
  4. 点击Save & test

你应该看到"Data source successfully connected."。如果失败,验证两个容器是否在同一个Docker网络(loki-net)上。

运行你的第一个查询

  1. 进入Explore(侧边栏中的指南针图标)
  2. 选择Loki数据源
  3. 切换到Code模式(不是Builder),输入此LogQL查询:
{job="systemd-journal"} |= "ssh"

这显示所有包含"ssh"的systemd journal条目。如果你看到日志行,完整管道正在工作:journal -> Promtail -> Loki -> Grafana。

尝试Docker容器查询:

{compose_service="loki"}

这返回Loki自己的日志,由Promtail通过Docker服务发现收集。

以及Nginx查询(如果Nginx已安装并正在生成日志):

{job="nginx", type="error"}

如果Grafana显示"No data",等待2-3分钟。Loki需要时间来摄入和索引第一批日志。

最有用的服务器日志LogQL查询有哪些?

LogQL有两种查询类型:日志查询返回日志行,指标查询返回数值。两者都以流选择器({label="value"})开始,选择要扫描的日志流,然后添加过滤器和解析器来细化结果。

流选择器和行过滤器

# SSH守护进程的所有日志
{unit="sshd.service"}

# 包含"Failed password"的行
{unit="sshd.service"} |= "Failed password"

# 不包含"Accepted"的行
{unit="sshd.service"} != "Accepted"

# 正则匹配:IP地址
{unit="sshd.service"} |~ "\\d+\\.\\d+\\.\\d+\\.\\d+"

# 不区分大小写匹配
{job="nginx", type="error"} |~ "(?i)timeout"

行过滤器(|=!=|~!~)在流选择器之后执行。它们扫描日志行内容。多个过滤器可以链式使用,所有条件必须匹配:

{unit="sshd.service"} |= "Failed" |= "root"

这找出同时包含"Failed"和"root"的行。

解析器

解析器从非结构化日志行中提取结构化字段。解析后,你可以按提取的字段过滤,如status >= 500,而不是使用正则匹配。根据日志格式选择合适的解析器:

解析器 语法 适用于 性能说明
logfmt | logfmt 键值对日志(systemd、Go应用) 最快。无正则。
json | json JSON结构化日志 快速。原生JSON解析。
pattern | pattern "<pattern>" 固定格式日志(Nginx combined、Apache) 快速。位置提取。
regexp | regexp "<regex>" 不规则格式、混合结构 最慢。作为最后手段使用。

当日志已有结构时使用logfmtjson。对于已知格式如Nginx combined log使用pattern。只在其他方法都不行时使用regexp,因为正则解析在高流量stream上明显更慢。

Nginx访问日志配合pattern解析器:

{job="nginx", type="access"}
  | pattern "<ip> - - [<timestamp>] \"<method> <uri> <_>\" <status> <bytes>"
  | status >= 500

这将Nginx combined日志格式解析为命名字段并过滤5xx错误。<_>占位符丢弃不需要的字段(此处为HTTP版本)。

JSON日志解析器:

{compose_service="myapp"}
  | json
  | level="error"
  | line_format "{{.timestamp}} {{.message}}"

line_format阶段重新格式化输出。当JSON日志过于冗长,你想在Grafana中获得更清晰的输出时很有用。

从日志生成指标

指标查询将日志行转换为数值。它们驱动Grafana仪表板和告警规则:

# 过去一小时内每分钟SSH登录失败率
rate({unit="sshd.service"} |= "Failed password" [5m])

# 5分钟窗口内Nginx 5xx错误总数
count_over_time(
  {job="nginx", type="access"}
    | pattern "<_> - - [<_>] \"<_> <_> <_>\" <status> <_>"
    | status >= 500
  [5m]
)

# 从JSON应用日志中获取P95响应时间
# unwrap提取用于聚合的数值字段
quantile_over_time(0.95,
  {compose_service="myapp"}
    | json
    | unwrap response_time_ms
    | __error__=""
  [5m]
) by (endpoint)

# Nginx每秒传输的字节数
sum(rate(
  {job="nginx", type="access"}
    | pattern "<_> - - [<_>] \"<_> <_> <_>\" <_> <bytes>"
    | unwrap bytes
    | __error__=""
  [5m]
))

unwrap后的| __error__=""过滤器丢弃数值提取失败的行(非数值、缺失字段)。没有它,这些行会静默产生零值并扭曲你的结果。始终在unwrap后包含此过滤器。

[5m]范围定义窗口大小。较短的范围(1m)给出更细粒度但更嘈杂的数据。较长的范围(15m、1h)平滑尖峰。对于仪表板,5m是一个好的起点。

如何在Loki 3.x中设置日志保留?

在Loki 3.x中,保留由compactor管理。在limits_config下设置retention_period,并通过retention_enabled: true启用compactor。Loki 3.0+的默认保留是0s(永久保留),因此你必须显式配置,否则磁盘会被填满。

上面的loki-config.yml已经包含了保留配置。以下是各设置如何协作:

compactor:
  retention_enabled: true        # 必须为true,否则compactor只压缩(不删除)
  retention_delete_delay: 2h     # 标记chunks后等待2小时再删除
  compaction_interval: 10m       # compactor运行频率

limits_config:
  retention_period: 720h         # 30天全局默认值

在单节点模式下,compactor作为Loki进程的一部分运行。它扫描TSDB索引,识别超过保留期的chunks,标记它们待删除,然后在retention_delete_delay后移除。延迟给你一个恢复窗口,以防保留配置错误。

按流设置保留

你可以为不同的日志流设置不同的保留期。高流量、低价值的日志(如debug输出)可以更快过期:

limits_config:
  retention_period: 720h
  retention_stream:
    - selector: '{job="nginx", type="access"}'
      priority: 1
      period: 336h    # 访问日志保留14天
    - selector: '{severity="debug"}'
      priority: 2
      period: 72h     # debug日志保留3天

当多个选择器匹配同一个stream时,优先级值较高的获胜。一条debug级别的Nginx访问日志匹配两条规则。优先级2获胜,所以保留3天。

保留容量规划

在确定保留期之前估算磁盘使用量。Loki压缩日志效果好(Snappy下5-10倍压缩比),但在繁忙的服务器上数字会累积:

每日原始日志量 压缩后(估计) 7天保留 30天保留 90天保留
100 MB ~15 MB ~105 MB ~450 MB ~1.35 GB
500 MB ~75 MB ~525 MB ~2.25 GB ~6.75 GB
1 GB ~150 MB ~1.05 GB ~4.5 GB ~13.5 GB
5 GB ~750 MB ~5.25 GB ~22.5 GB ~67.5 GB

这些是估计值。实际压缩取决于日志内容。重复性日志(具有相似路径的访问日志)比随机debug输出压缩效果更好。用以下命令监控实际使用量:

du -sh ~/loki-stack/loki-data/

每周运行此命令,在磁盘空间耗尽之前发现意外增长。

如何在单台VPS上为生产环境调优Loki?

默认的Loki配置适合测试,但生产VPS需要调优。以下变更减少内存峰值、防止失控的日志流,并为面向公网的服务器加固技术栈。

将Loki绑定到localhost

已在上面的docker-compose.yml中完成(127.0.0.1:3100:3100)。部署后二次确认:

ss -tlnp | grep 3100

你应该看到127.0.0.1:3100,而非0.0.0.0:3100。对Grafana的3000端口也做同样检查。

防火墙规则

如果你使用ufw,阻止对日志端口的外部访问:

sudo ufw deny 3100/tcp comment "Loki - localhost only"
sudo ufw deny 3000/tcp comment "Grafana - localhost only"
sudo ufw status numbered

由于端口已经绑定到localhost,防火墙是纵深防御。如果有人意外修改compose文件绑定到0.0.0.0,防火墙仍然阻止外部访问。

隐藏版本信息

版本泄露帮助攻击者针对已知漏洞。compose文件中的GF_ANALYTICS_REPORTING_ENABLED=false已经禁用了Grafana遥测。Loki的/loki/api/v1/status/buildinfo端点暴露版本详情,但由于Loki绑定到localhost,只有本地进程可以访问。

如果你将Grafana放在反向代理(Nginx、Caddy)后面,添加以下Nginx设置:

server_tokens off;
proxy_hide_header X-Powered-By;

Loki 3.x重大变更

如果你从Loki 2.x配置迁移或遵循旧教程,注意以下变更:

变更 Loki 2.x默认值 Loki 3.x默认值 需要的操作
Schema v11/v12 v13 使用schema: v13配合store: tsdb
索引存储 boltdb-shipper tsdb 迁移到TSDB(BoltDB已弃用)
保留 0s(永久保留) 0s(永久保留) 显式设置retention_period
结构化元数据 禁用 启用 需要v13 schema
每系列最大标签数 30 15 降低标签基数或提高限制
Docker镜像 包含BusyBox shell 无shell 无法通过docker exec进入容器

Loki 3.6+的无shell Docker镜像意味着你不能运行docker exec -it loki sh进行调试。改为使用docker compose logs loki检查日志,curl http://127.0.0.1:3100/ready检查就绪状态。

文件系统与对象存储

对于单台VPS,文件系统存储是正确的选择。对象存储(S3、GCS、MinIO)增加复杂性和延迟,只有在以下情况才值得:

  • 多个Loki实例共享相同数据
  • 需要超出VPS磁盘的无限存储
  • 跨区域复制

在你超出单节点能力之前,坚持使用文件系统存储。

监控技术栈

检查运行中容器的资源使用情况:

docker stats --no-stream

检查Loki日志中的警告和错误:

docker compose logs loki --tail=50 | grep -E "level=(error|warn)"

常见问题和解决方案:

  • stream limit exceeded:增加limits_config中的max_streams_per_user。通常由高基数标签引起。
  • ingestion rate limit reached:增加ingestion_rate_mb。在日志突增时发生(部署、错误风暴)。
  • WAL replay:启动时正常。Loki正在从预写日志恢复未提交的写入。
  • 高内存使用:减小chunk_cache_config中的max_size_mb或降低ingestion_burst_size_mb

Promtail是否已弃用?应该使用Grafana Alloy吗?

是的。Promtail于2025年2月13日进入长期支持,于2026年3月2日到达生命周期终点。不再发布任何更新、错误修复或安全补丁。Grafana Alloy是官方替代品。它是Grafana Labs发行的OpenTelemetry Collector,在单个代理中处理日志、指标、追踪和性能分析数据。

为什么本教程仍使用Promtail

Promtail的配置概念直接映射到Alloy。scrape configs、relabel规则和pipeline阶段的工作方式相同。学习Promtail仍然有用,因为:

  1. 数百万现有部署在使用它
  2. Alloy迁移工具自动转换Promtail配置
  3. 理解Promtail使调试Alloy配置更容易
  4. Promtail教程的搜索量仍然很高,且概念可迁移

迁移到Alloy

一条命令将Promtail配置转换为Alloy格式:

alloy convert --source-format=promtail --output=alloy-config.alloy promtail-config.yml

这会生成Alloy格式的配置文件。部署前检查输出。转换器处理大多数情况,但自定义pipeline阶段可能需要手动调整。

然后替换docker-compose.yml中的Promtail服务:

  alloy:
    image: grafana/alloy:v1.14.1
    command:
      - run
      - /etc/alloy/config.alloy
      - --server.http.listen-addr=0.0.0.0:12345
    volumes:
      - ./alloy-config.alloy:/etc/alloy/config.alloy:ro
      - /var/log:/var/log:ro
      - /var/log/journal:/var/log/journal:ro
      - /run/log/journal:/run/log/journal:ro
      - /etc/machine-id:/etc/machine-id:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
    restart: unless-stopped
    depends_on:
      - loki
    networks:
      - loki-net

移除promtail服务块并运行docker compose up -d。Alloy将使用转换后的配置开始收集相同的日志源。

对于从零开始的新部署,直接使用Alloy。对于现有Promtail安装,规划迁移但不必急于一时。Promtail二进制文件继续可用。固定镜像标签(grafana/promtail:3.6.7)以控制运行的版本。

如何通过HTTP API程序化查询Loki日志?

Loki提供REST API用于程序化日志查询。这是你将Loki与脚本、告警管道或在VPS上使用Ollama进行AI日志分析:用本地LLM检测异常中介绍的AI日志分析层集成的方式。

API接受与Grafana中相同的LogQL查询。主要端点是/loki/api/v1/query_range,用于时间范围查询。

查询最近的日志

END=$(date +%s)000000000
START=$(( $(date +%s) - 3600 ))000000000

curl -s "http://127.0.0.1:3100/loki/api/v1/query_range" \
  --data-urlencode "query={unit=\"sshd.service\"} |= \"Failed password\"" \
  --data-urlencode "start=$START" \
  --data-urlencode "end=$END" \
  --data-urlencode "limit=50" | jq .

startend参数使用Unix纳秒时间戳。上面的shell运算计算"当前时间减1小时"并附加9个零以获得纳秒精度。

响应为JSON格式:

{
  "status": "success",
  "data": {
    "resultType": "streams",
    "result": [
      {
        "stream": {"unit": "sshd.service", "severity": "info"},
        "values": [
          ["1710850000000000000", "Mar 19 14:00:00 vps sshd[1234]: Failed password for root from 203.0.113.5 port 22"]
        ]
      }
    ]
  }
}

每个值是一个[纳秒时间戳, 日志行]对。这正是你将日志输入本地LLM进行分析时要解析的格式。

查询标签和流

列出所有标签名称:

curl -s "http://127.0.0.1:3100/loki/api/v1/labels" | jq .

列出特定标签的值:

curl -s "http://127.0.0.1:3100/loki/api/v1/label/unit/values" | jq .

这些端点对于在自动化脚本中构建动态查询很有用。你可以枚举所有unit,然后查询每个unit的错误。

即时查询

对于不需要时间范围的"当前"查询,使用/loki/api/v1/query

curl -s "http://127.0.0.1:3100/loki/api/v1/query" \
  --data-urlencode 'query=count_over_time({job="systemd-journal"} |= "error" [1h])' | jq .

这返回单个数据点:过去一小时的journal错误计数。对于健康检查和监控脚本很有用。

故障排除

Promtail显示Docker socket的"permission denied":

Promtail容器需要对/var/run/docker.sock的读取权限。检查主机上socket的权限:

ls -la /var/run/docker.sock

socket通常属于root:docker。Promtail镜像默认以root运行,所以通常可以正常工作。如果你以自定义用户运行Promtail,该用户必须在docker组中。

没有journal日志出现:

首先,检查Promtail日志中是否有support for reading the systemd journal is not compiled into this build of promtail消息。如果看到这条消息,Docker镜像不支持journal抓取。安装Promtail作为主机二进制文件或切换到Grafana Alloy(参见上面的章节)。

如果Promtail以支持journal的主机二进制文件运行,验证journal目录是否存在:

ls -la /var/log/journal/

如果不存在,systemd正在使用易失性(仅内存)journal存储。启用持久化存储:

sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald

然后更新docker-compose.yml中的Promtail卷挂载,从/run/log/journal改为/var/log/journal并重启:

docker compose up -d promtail

Loki报告"too many outstanding requests":

查询负载超出Loki容量。缩小查询的时间范围或添加查询限制:

limits_config:
  max_query_parallelism: 16
  max_query_series: 500

Grafana显示"Data source connected, no labels found":

Loki需要几分钟来摄入和索引首批日志。等待2-3分钟,然后重试查询。验证Loki是否就绪:

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

一次查看所有服务日志:

docker compose logs -f --tail=50

这跟踪所有三个服务。过滤问题:

docker compose logs --tail=100 | grep -i error

后续步骤

你的Loki管道正在收集systemd、Docker和Nginx的日志。你可以在Grafana中使用LogQL查询它们,或通过HTTP API查询。

接下来可以:


版权所有 2026 Virtua.Cloud。保留所有权利。 本内容为 Virtua.Cloud 团队原创作品。 未经书面许可,禁止复制、转载或再分发。

准备好亲自尝试了吗?

几秒内部署您自己的服务器。支持 Linux、Windows 或 FreeBSD。

查看 VPS 方案