AL2023 上搭 ALGA 日志平台:日志已经进了 S3,告警为什么还是没响
平台上线第三天,Grafana 里查日志很顺:Nginx 访问日志、systemd 日志都能看到,S3 存储桶里也开始出现对象。大家都以为日志平台已经能用了。
接着做一次演练:让测试接口连续返回 5xx。Grafana 里能看到错误数在涨,告警群里却没有任何消息。Alertmanager 页面也是空的,看上去一切正常。
这类问题很少报错,所以更危险。日志采集、存储、查询、规则评估、告警投递是五段独立链路,每段都可能单独失败。查询正常,只能证明前三段大概率是通的。
我的核心观点是:日志平台上线那天,要验证的除了能不能查到日志,还有过期数据能不能删掉、出故障时能不能把人叫醒。
下面以 Amazon Linux 2023 为例,整理一套 ALGA 单机部署和验证方法:Alloy 负责采集,Loki 负责存储和查询,数据落到 AWS S3,Grafana 负责展示,Alertmanager 负责告警。Loki、Alloy、Alertmanager 使用二进制安装,Grafana 使用 dnf 安装。
先看清这套链路里谁负责什么
这里有一个常被误解的地方:Alertmanager 本身不查日志。它只接收别人推过来的告警,再负责分组、去重、抑制和发送。真正执行 LogQL 告警规则的是 Loki 里的 ruler。
所以告警不响时,不要一开始就改 Alertmanager 的接收人。先确认 Loki 是否加载了规则、规则是否评估成功、告警是否已经进入 firing 状态,再看 Loki 能否把告警推给 Alertmanager。
先在 ALG 主机上确认系统、架构和组件版本。这一步是只读操作:
cat /etc/os-release | grep -E '^(PRETTY_NAME|VERSION_ID)='
uname -m
getenforce
systemctl is-active firewalld 2>/dev/null || echo "firewalld not active"
aws --version
/usr/local/bin/loki -version
/usr/local/bin/alloy --version
/usr/local/bin/alertmanager --version
rpm -q grafana结果按下面几项判断:
uname -m返回x86_64时,下载amd64包;返回aarch64时,下载arm64包。Graviton 机型很容易下错包。- AL2023 默认通常是 SELinux
Permissive。如果你们的基线改成了Enforcing,服务启动失败时先查 SELinux 审计日志,再考虑调整权限。 - AL2023 默认通常不启用 firewalld。端口访问主要由安全组控制。
- 后面的配置写法按 Loki 3.x 和 TSDB 存储编写。如果
loki -version显示的是 2.x,不要直接套用。
二进制安装时,我建议保留带版本号的文件,再用软链接指向当前版本。以 Loki 为例:
LOKI_VER=
cd /tmp
curl -fLO https://github.com/grafana/loki/releases/download/v${LOKI_VER}/loki-linux-amd64.zip
unzip -o loki-linux-amd64.zip
install -m 0755 loki-linux-amd64 /usr/local/bin/loki-${LOKI_VER}
ln -sfn /usr/local/bin/loki-${LOKI_VER} /usr/local/bin/loki下载后,对照 Release 页面提供的校验文件核对 sha256。升级出问题时,把软链接切回旧版本文件,然后重启服务即可回滚。Alloy 和 Alertmanager 也可以按这个方法管理。
S3 这一层,先用实例角色把权限跑通
不要把 Access Key 写进 Loki 配置文件。给 EC2 绑定 IAM 实例角色后,Loki 会通过 AWS SDK 的默认凭证链读取临时凭证。下面是一个最小权限示例:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::"
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::/*"
}
]
}s3:DeleteObject 不能省。Loki 的 compactor 合并索引时,以及执行保留期删除时,都需要删除对象。如果存储桶使用 SSE-KMS,还要给实例角色和 KMS 密钥策略都放通 kms:GenerateDataKey 与 kms:Decrypt。
在 ALG 主机上验证身份和存储桶访问。以下命令都是只读操作:
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 300")
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/
aws sts get-caller-identity
aws s3 ls s3:///
aws s3api get-public-access-block --bucket 第二条 curl 只会返回角色名。不要继续访问后面的凭证路径,那里会输出临时密钥,容易被贴进工单或聊天记录。
判断方法如下:
get-caller-identity中的 ARN 应该是预期的实例角色。aws s3 ls返回AccessDenied时,先检查角色策略、存储桶策略和 KMS 策略,再改 Loki 配置。- 存储桶的四项 Block Public Access 都应为
true。
如果 ALG 主机在私有子网,建议配置 S3 Gateway Endpoint,让 S3 流量不经过 NAT 网关。可以先只读检查是否已经存在:
aws ec2 describe-vpc-endpoints \
--filters Name=service-name,Values=com.amazonaws..s3 \
--query 'VpcEndpoints[].{id:VpcEndpointId,type:VpcEndpointType,rt:RouteTableIds}'Loki 单机配置:存储、保留期和 ruler 放在一起看
先创建运行用户和数据目录。这一步会改变系统状态,但影响范围很小:
useradd --system --no-create-home --shell /sbin/nologin loki
mkdir -p /etc/loki/rules/fake /var/lib/loki
chown -R loki:loki /var/lib/loki /etc/loki/rules下面是 /etc/loki/config.yaml 示例。存储桶名称、区域和日期都需要替换:
auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
log_level: info
common:
instance_addr: 127.0.0.1
path_prefix: /var/lib/loki
replication_factor: 1
ring:
kvstore:
store: inmemory
storage:
s3:
region:
bucketnames:
schema_config:
configs:
- from: "2026-11-01"
store: tsdb
object_store: s3
schema: v13
index:
prefix: index_
period: 24h
storage_config:
tsdb_shipper:
active_index_directory: /var/lib/loki/tsdb-index
cache_location: /var/lib/loki/tsdb-cache
compactor:
working_directory: /var/lib/loki/compactor
compaction_interval: 10m
retention_enabled: true
retention_delete_delay: 2h
delete_request_store: s3
limits_config:
retention_period: 720h
reject_old_samples: true
reject_old_samples_max_age: 168h
ruler:
storage:
type: local
local:
directory: /etc/loki/rules
rule_path: /var/lib/loki/rules-temp
alertmanager_url: http://127.0.0.1:9093
enable_api: true这份配置里有几处值得重点看:
auth_enabled: false时,Loki 使用固定租户fake。所以规则文件要放在/etc/loki/rules/fake/下面,不能直接放在/etc/loki/rules/。alertmanager_url只写 Alertmanager 的根地址。Loki 会自己拼接告警接口路径。schema_config一旦写入数据就不要修改。以后要换存储结构时,新增一条from为未来日期的配置。直接改旧配置,可能导致历史数据查不出来。- 开启
retention_enabled后,必须配置delete_request_store,否则 Loki 3.x 会在启动校验阶段报错。
启动前,先校验配置:
loki -config.file=/etc/loki/config.yaml -verify-config
loki -config.file=/etc/loki/config.yaml -print-config-stderr 2>&1 | grep -E 'retention_period|alertmanager_url|bucketnames'第二条命令显示的是 Loki 实际生效的配置。它可以帮你发现字段缩进错误:YAML 能解析,但字段放错层级后,Loki 可能按默认值运行。
下面是 systemd 服务文件 /etc/systemd/system/loki.service:
[Unit]
Description=Grafana Loki
After=network-online.target
Wants=network-online.target
[Service]
User=loki
Group=loki
ExecStart=/usr/local/bin/loki -config.file=/etc/loki/config.yaml
Restart=on-failure
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target启动服务并检查状态:
systemctl daemon-reload
systemctl enable --now loki
curl -s http://127.0.0.1:3100/ready
journalctl -u loki --since "10 min ago" --no-pager | grep -Ei 'error|denied|s3' | tail -20/ready 返回 ready 才表示 Loki 已经可以接收和查询数据。
刚启动时 S3 里还没有对象,这通常正常。Loki 会先把日志保存在本地 WAL 和内存中,等 chunk 空闲或达到最大存活时间后,再上传到 S3。按默认配置,首批对象可能要等几十分钟到两小时才出现。
这也意味着本地磁盘里会有一段尚未上传的数据。实例和 EBS 卷一起丢失时,这部分日志也会丢失。这是单机方案需要接受的边界。
Alloy 采集:权限、标签和读取位置
Alloy 也建议用独立用户运行。读取 journald 需要加入 systemd-journal 组。读取 Nginx 日志可以用 ACL 授权:
useradd --system --no-create-home --shell /sbin/nologin alloy
mkdir -p /etc/alloy /var/lib/alloy/data
chown -R alloy:alloy /var/lib/alloy
setfacl -m u:alloy:rx /var/log/nginx
setfacl -d -m u:alloy:r /var/log/nginx
setfacl -m u:alloy:r /var/log/nginx/*.log
sudo -u alloy head -n 1 /var/log/nginx/access.log最后一条命令能输出一行日志,才说明权限已经生效。setfacl -d 用于让 logrotate 新建的日志文件继承读取权限。轮转后再执行一次 sudo -u alloy head,确认新文件也能读取。
下面是 /etc/alloy/config.alloy 示例:
local.file_match "nginx" {
path_targets = [
{__path__ = "/var/log/nginx/access.log", job = "nginx_access", host = constants.hostname},
{__path__ = "/var/log/nginx/error.log", job = "nginx_error", host = constants.hostname},
]
}
loki.source.file "nginx" {
targets = local.file_match.nginx.targets
forward_to = [loki.process.drop_health.receiver]
}
loki.process "drop_health" {
stage.drop {
expression = ".*GET /healthz.*"
}
forward_to = [loki.write.default.receiver]
}
loki.relabel "journal" {
forward_to = []
rule {
source_labels = ["__journal__systemd_unit"]
target_label = "unit"
}
}
loki.source.journal "system" {
max_age = "12h"
relabel_rules = loki.relabel.journal.rules
labels = {job = "systemd-journal", host = constants.hostname}
forward_to = [loki.write.default.receiver]
}
loki.write "default" {
endpoint {
url = "http://127.0.0.1:3100/loki/api/v1/push"
}
}这里把访问日志和错误日志拆成两个 job。如果把它们混在一起,后面计算 5xx 比例时,错误日志也会被算进总请求数,比例就不准了。
status、uri、trace_id 这类变化很多的字段不要做成 Loki 标签。它们会产生大量日志流,影响写入和查询。更合适的做法是在查询时再解析。
启动前先检查配置格式:
alloy fmt /etc/alloy/config.alloysystemd 服务里的关键部分如下:
[Service]
User=alloy
Group=alloy
SupplementaryGroups=systemd-journal
ExecStart=/usr/local/bin/alloy run --server.http.listen-addr=127.0.0.1:12345 --storage.path=/var/lib/alloy/data /etc/alloy/config.alloy
Restart=on-failure--storage.path 保存文件读取位置,也就是 positions。Alloy 重启后会从上次读到的位置继续读取。
修改组件名称要特别小心,例如把 loki.source.file "nginx" 改成其他名字。组件名变化后,Alloy 可能把它当成一个新组件,重新读取旧日志,造成重复写入。
服务启动后,验证 Alloy 是否在发送日志:
curl -s http://127.0.0.1:12345/metrics | grep -E 'loki_write_(sent|dropped)_entries_total'
journalctl -u alloy --since "10 min ago" --no-pager | grep -Ei '429|error|permission' | tail
logcli --addr=http://127.0.0.1:3100 query --since=10m --limit=3 '{job="nginx_access"}'判断方法如下:
sent持续增长,dropped不增长,说明 Alloy 在正常发送。- Alloy 日志里出现 HTTP 429,通常是写入速度超过了 Loki 的租户限流。
单租户场景下,所有主机共享 fake 租户的写入额度。接入更多机器时,先接一台机器观察,再逐步扩大。必要时,在 limits_config 中小步调高 ingestion_rate_mb 和 ingestion_burst_size_mb。
告警链路:规则、投递和接收要分开验证
先配置 Alertmanager。单机部署时关闭集群端口,并只监听本机地址:
alertmanager --config.file=/etc/alertmanager/alertmanager.yml \
--storage.path=/var/lib/alertmanager \
--web.listen-address=127.0.0.1:9093 \
--cluster.listen-address=下面是 /etc/alertmanager/alertmanager.yml 示例:
route:
receiver: default-webhook
group_by: ['alertname', 'host']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
- matchers: ['severity="critical"']
receiver: oncall-webhook
repeat_interval: 1h
receivers:
- name: default-webhook
webhook_configs:
- url: http:///alertmanager/default
send_resolved: true
- name: oncall-webhook
webhook_configs:
- url: http:///alertmanager/oncall
send_resolved: true
inhibit_rules:
- source_matchers: ['severity="critical"']
target_matchers: ['severity="warning"']
equal: ['alertname', 'host']修改配置后,先校验配置和路由,再热加载:
amtool check-config /etc/alertmanager/alertmanager.yml
amtool config routes test --config.file=/etc/alertmanager/alertmanager.yml severity=critical
curl -s -X POST http://127.0.0.1:9093/-/reload
curl -s http://127.0.0.1:9093/api/v2/status | jq '.config.original' | headroutes test 应该返回 oncall-webhook。如果返回的是默认接收人,说明 matchers 写错了。
热加载失败时,Alertmanager 会继续使用旧配置,并在日志中报错。所以 /-/reload 之后要查看日志或状态接口,确认新配置已经生效。
接着在 Loki 中写告警规则。规则文件路径是 /etc/loki/rules/fake/nginx.yaml:
groups:
- name: nginx-log-alerts
interval: 1m
rules:
- alert: NginxHigh5xxRatio
expr: |
sum by (host) (count_over_time({job="nginx_access"}
| pattern `<_> - <_> [<_>] "<_> <_> <_>" <_>`
| status=~"5.." [5m]))
/
sum by (host) (count_over_time({job="nginx_access"}[5m])) > 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "{{ $labels.host }} 5xx 比例超过 5%"
- alert: NginxAccessLogAbsent
expr: absent_over_time({job="nginx_access"}[15m])
for: 5m
labels:
severity: warning
annotations:
summary: "15 分钟没有收到 nginx 访问日志"示例中的 5% 只是演示阈值。生产环境要按历史 5xx 比例和业务容忍度来设置。上线前,先用 logcli instant-query 单独执行分子和分母,确认数量级合理,再把表达式写进规则文件。
日志断流告警很有价值。采集端故障时,5xx 告警不会触发,因为根本没有日志参与计算,系统看起来会显得很安静。
最后做一次不影响业务的端到端演练。临时添加下面的规则组,并给它打上 severity: info 标签。演练前要确认 info 级别会发送到测试接收人:
- name: alga-drill
interval: 1m
rules:
- alert: AlgaDrill
expr: sum(count_over_time({job="systemd-journal"} |= "ALGA_DRILL" [5m])) > 0
labels:
severity: info演练过程分三段验证:
logger -t alga-drill "ALGA_DRILL $(date +%s)"
curl -s http://127.0.0.1:3100/prometheus/api/v1/rules | jq '.data.groups[].rules[] | {name,state,health,lastError}'
curl -s http://127.0.0.1:3100/metrics | grep -E 'notifications_(sent|errors|dropped)_total'
amtool alert query --alertmanager.url=http://127.0.0.1:9093 alertname=AlgaDrill判断方法如下:
- 规则的
health为ok,并在一两个评估周期内变成firing,说明规则评估正常。 - Loki 发送通知的计数增长,而且错误计数不增长,说明 Loki 已经把告警推给 Alertmanager。
amtool能查到AlgaDrill,并且测试接收人收到了消息,说明告警发送链路正常。
演练结束后,删除这组规则。
一次告警没响的复盘
下面用一个脱敏后的典型场景说明,时间和数值均为示例。
示例时间 14:00,团队做 5xx 演练。Grafana 中 nginx_access 的 5xx 曲线明显上升,按示例数据,比例约为 18%,持续 10 分钟。但是告警群没有收到消息,Alertmanager 页面也没有活动告警。
这次演练没有影响真实用户,但它暴露出一个风险:如果发生真实故障,值班人员不会收到通知。
第一个怀疑方向是 Alertmanager 或 Webhook。为了验证这一点,我用 amtool alert add alertname=AmPathTest severity=info --alertmanager.url=http://127.0.0.1:9093 直接向 Alertmanager 注入一条测试告警。测试接收人在一分钟内收到了消息,所以 Alertmanager 到 Webhook 这一段被排除。
第二步检查 Loki 是否加载了规则。查询 /prometheus/api/v1/rules 后,返回的 groups 是空数组。查看文件位置,规则文件被放在 /etc/loki/rules/nginx.yaml,缺少 fake/ 这一级租户目录。
把规则移动到 /etc/loki/rules/fake/ 后,规则组出现了。几分钟后,规则状态从 pending 变成 firing。
但是 Alertmanager 仍然没有收到告警。继续查看 Loki 指标,发送通知的错误计数持续增长。Loki 日志中出现发送告警失败的 404 记录。
最后检查配置,发现 alertmanager_url 写成了 http://127.0.0.1:9093/api/v2/alerts。Loki 会在这个地址后面继续拼接告警接口路径,最终请求到了一个不存在的地址。
修复步骤如下:
- 备份配置:
cp -a /etc/loki/config.yaml /etc/loki/config.yaml.bak-$(date +%F-%H%M)。 - 把
alertmanager_url改为根地址,然后执行-verify-config。 - 在低峰时段重启 Loki,并观察
/ready、Alloy 的 429 和 dropped 指标。 - 如果 10 分钟内 Loki 不能恢复 ready,或者写入错误持续增长,就恢复备份配置并重启。
修复后再次演练,告警在 for: 5m 等待期结束后进入 Alertmanager,并发送到正确的接收人。为了防止同类问题再次发生,团队把 AlgaDrill 加入每次变更后的验收步骤,并增加了一条告警:Loki 发送告警通知的错误计数持续增长时通知值班人员。
这次复盘的结论有两点:Loki 规则目录缺少租户层级,导致规则没有加载;alertmanager_url 写了完整接口路径,导致告警投递返回 404。这两个结论分别由空规则组、通知错误计数和 404 日志支撑。
上线前收藏这份检查清单
- 不要把 AK/SK 写进 Loki 配置:配置文件进入 Git、备份或工单后,长期凭证会扩散。务必改用实例角色。
- 千万别漏掉
s3:DeleteObject权限:缺少这个权限时,compactor 无法完成清理,保留期配置会失效,S3 费用会一直增加。 - 务必确认 S3 生命周期规则只做兜底:过期天数要明显大于
retention_period。如果先删除了 chunk,查询时可能出现数据缺失。 - 开启版本控制时,记得清理非当前版本:Loki 删除的对象会变成旧版本,存储容量不会真正下降。
- 禁止直接修改已写入数据的
schema_config:要新增一条未来日期的配置,否则历史数据可能无法读取。 - 不要把高基数字段做成标签:日志流数量暴涨后,写入、查询和内存都会受影响。
- 务必把规则放进
fake/目录:在auth_enabled: false时,放错层级不会明显报错,规则只是不会加载。 alertmanager_url只写根地址:写完整接口路径会导致路径重复,最终返回 404。- 不要只配置阈值告警:采集端中断时阈值告警不会触发,还需要配置
absent_over_time断流告警。 - 不要把 Loki 3100 端口暴露到公网:关闭认证后,任何能访问该端口的人都可以写入或查询日志。务必用安全组限制来源网段。
- 务必锁定 Grafana 版本:执行
dnf versionlock add grafana,避免日常dnf update带来未经验证的大版本升级。
Grafana 使用 dnf 安装时,先按官方方式配置 rpm.grafana.com 仓库和 GPG key,再执行 dnf install grafana 和 systemctl enable --now grafana-server。数据源建议通过 /etc/grafana/provisioning/datasources/ 管理:Loki 数据源指向 http://127.0.0.1:3100,Alertmanager 数据源的 implementation 设置为 prometheus。这样重建 Grafana 时,可以减少手工配置。
这套 ALGA 方案的关键,是把每一段链路分别验证清楚:S3 能写入也能删除,Alloy 能读取文件并记住读取位置,Loki 能加载并评估规则,Alertmanager 能把告警发送给正确的人。
今天就可以做一件事:在你的环境里写入一条 logger -t alga-drill "ALGA_DRILL",从日志一路检查到告警消息。只有完成整条链路演练,日志平台上线才算真正收尾。
如果这篇文章对你有帮助,欢迎关注、点赞、点“在看”,也欢迎转发给正在搭日志平台的同事。
公众号:耕云躬行录
个人博客:躬行笔记