在VPS上使用Grafana Loki实现集中式日志管理
通过Docker Compose在单台VPS上部署Grafana Loki、Promtail和Grafana。收集systemd、Docker和Nginx日志,使用LogQL查询,并配置日志保留策略。
分散在/var/log和docker 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.yml、loki-config.yml和promtail-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.7、11.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_id或request_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.service、nginx.service)。__journal_priority_keyword变成severity(如warning、err、info)。这些标签让你在LogQL中高效过滤日志,无需逐行扫描。
要使journal抓取正常工作,Promtail容器需要两个卷挂载:/run/log/journal(或者如果你的系统持久化日志则是/var/log/journal)和/etc/machine-id。machine ID用于标识要读取的journal。
Docker镜像限制: 标准
grafana/promtailDocker镜像未编译systemd journal支持。如果你在日志中看到support for reading the systemd journal is not compiled into this build of promtail,journal抓取将无法在Docker容器中工作。你有两个选择:
- 将Promtail作为主机二进制文件安装,而非使用Docker镜像。从Loki releases页面下载,直接在主机上运行,这样它可以原生访问journal API。
- 使用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标签(stdout或stderr)。__meta_docker_container_label_com_docker_compose_service提取Compose服务名(如loki、grafana)。此标签仅存在于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: access和type: error标签。
启动技术栈
cd ~/loki-stack
docker compose up -d
验证三个容器都在运行:
docker compose ps
你应该看到loki、promtail和grafana的状态都是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为数据源
- 进入Connections > Data Sources > Add data source
- 选择Loki
- 将URL设置为
http://loki:3100(这是Docker内部主机名,不是localhost) - 点击Save & test
你应该看到"Data source successfully connected."。如果失败,验证两个容器是否在同一个Docker网络(loki-net)上。
运行你的第一个查询
- 进入Explore(侧边栏中的指南针图标)
- 选择Loki数据源
- 切换到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>" |
不规则格式、混合结构 | 最慢。作为最后手段使用。 |
当日志已有结构时使用logfmt或json。对于已知格式如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仍然有用,因为:
- 数百万现有部署在使用它
- Alloy迁移工具自动转换Promtail配置
- 理解Promtail使调试Alloy配置更容易
- 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 .
start和end参数使用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查询。
接下来可以:
- 在VPS上使用Ollama进行AI日志分析:用本地LLM检测异常 将这些日志输入本地LLM进行异常检测和自动化告警
- VPS上的AIOps:使用开源工具实现AI驱动的服务器管理 AIOps自托管技术栈概览
- [-> self-host-signoz-openobserve-vps] 如果你更倾向于一体化APM而非Grafana技术栈
版权所有 2026 Virtua.Cloud。保留所有权利。 本内容为 Virtua.Cloud 团队原创作品。 未经书面许可,禁止复制、转载或再分发。