从零搭一套可观测的最小日志栈:Loki + Promtail + Grafana
线上出问题时最痛苦的不是没日志,而是日志散在五台机器上,得挨个 ssh 上去 grep。这篇记录一次最小可用的日志聚合搭建:Loki 存日志、Promtail 采集、Grafana 查询,全部跑在一台 2 核 4G 的机器上,内存占用约 400MB。
为什么选 Loki 而不是 ELK
ELK(Elasticsearch + Logstash + Kibana)功能强,但代价也大:单机跑 ES 至少要 4G 内存,索引还会持续膨胀。Loki 的设计思路完全不同 —— 它只索引标签,不索引日志内容。
| 维度 | ELK | Loki |
|---|---|---|
| 索引对象 | 全文索引 | 仅标签(label) |
| 单机内存 | 4G 起步 | 200-500MB |
| 存储增长 | 快(索引膨胀) | 慢(压缩日志块) |
| 查询能力 | 全文检索强 | 标签筛选 + 正则 grep |
对个人项目和小团队来说,99% 的查询场景是「看某个服务最近半小时的日志」,用标签筛选完全够用。真要全文检索,Loki 也支持 |= 和 |~ 做行内过滤。
部署结构
三个组件用 docker compose 起,数据落在本地目录:
version: "3.8"
services:
loki:
image: grafana/loki:2.9.3
command: -config.file=/etc/loki/local-config.yaml
ports:
- "3100:3100"
volumes:
- ./loki-data:/loki
- ./loki-config.yaml:/etc/loki/local-config.yaml
restart: unless-stopped
promtail:
image: grafana/promtail:2.9.3
volumes:
- /var/log:/var/log:ro
- ./promtail-config.yaml:/etc/promtail/config.yaml
- /var/lib/docker/containers:/var/lib/docker/containers:ro
command: -config.file=/etc/promtail/config.yaml
depends_on:
- loki
restart: unless-stopped
grafana:
image: grafana/grafana:10.2.3
ports:
- "3000:3000"
volumes:
- ./grafana-data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=changeme
depends_on:
- loki
restart: unless-stopped
Loki 配置
单机模式,本地文件系统存储,关闭多副本:
auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
common:
instance_addr: 127.0.0.1
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: v12
index:
prefix: index_
period: 24h
limits_config:
retention_period: 168h # 保留 7 天
ingestion_rate_mb: 10
ingestion_burst_size_mb: 20
compactor:
working_directory: /loki/compactor
retention_enabled: true
delete_request_store: filesystem
关键参数说明:
retention_period: 168h—— 日志保留 7 天,超过自动删。不设这个磁盘会被吃满。replication_factor: 1—— 单机必须设 1,否则会等副本超时。ingestion_rate_mb—— 限制写入速率,防止某个服务刷日志把 Loki 打挂。
Promtail 采集配置
两路数据:系统日志和 Docker 容器日志。
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
# 系统日志
- job_name: system
static_configs:
- targets: [localhost]
labels:
job: syslog
host: vps-01
__path__: /var/log/*.log
# Docker 容器日志(自动发现)
- 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
pipeline_stages:
# 尝试解析 JSON 日志,提取 level 字段作为标签
- json:
expressions:
level: level
msg: message
- labels:
level:
docker_sd_configs 是自动发现,新起的容器不用改配置就能采到日志。pipeline_stages 里把 JSON 日志的 level 提成标签,这样就能在 Grafana 里按 {level="error"} 精确筛选。
一个容易踩的坑:标签基数爆炸
Loki 靠标签索引,但如果把高基数变量当标签,会直接把它拖死。典型错误:
# 千万不要这样写
labels:
request_id: ${request_id} # 每个请求都不同
user_id: ${user_id} # 用户量大了就炸
timestamp: ${ts} # 时间戳更不行
标签的基数应该控制在几百以内。request_id、user_id、trace_id 这类高基数字段只能放在日志正文里,查询时用正则匹配:
{job="docker"} |= "request_id=abc123"
判断标准很简单:如果一个标签的取值超过 1000 种,就不该做标签。
Grafana 查询示例
配好 Loki 数据源(URL 填 http://loki:3100)后,几个常用查询:
看某服务最近 10 分钟的错误日志:
{container="myapp"} |= "ERROR"
统计错误数量趋势(用于告警):
sum(count_over_time({container="myapp"} |= "ERROR" [1m]))
正则过滤多个关键词:
{job="syslog"} |~ "(?i)error|fail|timeout"
解析 JSON 后按字段筛选:
{container="myapp"} | json | level="error" | line_format "{{.msg}}"
line_format 很好用,可以把 JSON 日志只显示 msg 字段,避免一行里塞满元数据。
资源占用实测
跑在一台 2 核 4G、40G 盘的 Debian 12 上,日志量约每天 200MB:
| 组件 | 内存 | CPU |
|---|---|---|
| loki | ~180MB | 3% |
| promtail | ~45MB | 1% |
| grafana | ~120MB | 2% |
| 合计 | ~345MB | ~6% |
磁盘方面,200MB/天 × 7 天保留 ≈ 1.4GB,压缩后约 600MB。40G 盘完全够用。
加个告警
Grafana 里可以直接配告警规则,不用额外部署 Alertmanager:
sum(count_over_time({container="myapp"} |= "ERROR" [5m])) > 10
超过 10 条错误就触发,通知渠道支持邮件、Webhook、钉钉。Webhook 的 payload 是标准 JSON,接自己的机器人很方便。
小结
这套栈的价值不在技术复杂度,而在把「找日志」从 10 分钟降到 10 秒。三个容器 345MB 内存,一台最便宜的 VPS 就能跑。
几个关键决策点:
- 选 Loki 不选 ELK —— 单机场景内存差 10 倍,功能足够
- 必须设 retention —— 不设保留期磁盘必爆
- 标签基数控制在 1000 以内 —— 高基数字段放正文,用
|=过滤 - 用 docker_sd 自动发现 —— 新容器零配置接入
真正的门槛不是部署,是养成先看日志再猜原因的习惯。
- 点赞
- 收藏
- 关注作者
评论(0)