中小团队零成本自建监控体系,告警从200条/天降到5条,这套Prometheus+Loki+Grafana配置我用了3个月才跑通
每月云监控账单1.2万,告警消息一天200多条没人看,真出故障还是靠人肉SSH上去翻日志。50多个微服务跑在200个节点上,监控面板花里胡哨十几个Dashboard,值班的同学打开看一眼就关了——因为根本找不到重点。
这就是我们去年底的现状。
领导的原话是:"你们这个监控,到底是摆设还是真能用?"
我当时就觉得,这事不能再拖了。
旧架构有多拉胯
先说下当时的情况。公司业务增长快,微服务从年初的十几个膨胀到五十多个,峰值QPS大概1.2万,数据库TPS 8000左右。原来用的是某云厂商的监控服务,说实话刚开始还行,十来个服务的时候够用。但服务一多,问题就全暴露了:
- 自定义指标加一个得提工单等两天
- 告警规则只能选预置模板,想写个复杂点的PromQL?不支持
- 日志查询,5分钟之内的还凑合,跨一小时直接超时
- 最离谱的是计费——按时间线数量收费,服务一多时间线爆炸,账单跟着爆炸
上个季度有一次核心服务OOM,从告警触发到有人真正看到处理,中间隔了22分钟。22分钟啊,那会儿客户投诉都打到客服那边去了。
选型:为什么是这三件套
ELK我之前搞过,ES的资源消耗是真的离谱。之前那套ES集群光机器成本每月2万多,日志量还没现在大。Datadog之类的SaaS方案更贵,而且敏感数据放外面领导不同意。
最后选了Prometheus + Loki + Grafana:
- Prometheus — K8s生态下采集时序指标基本没对手,PromQL灵活度天花板
- Loki — 不做全文索引只索引label,存储成本能压到ES的1/10
- Grafana — 可视化天花板,而且这三个是同一个生态,集成度天然好
选型容易,落地才是噩梦的开始。
Prometheus部署:从安装到踩坑
基础部署
我们用的是kube-prometheus-stack这个Helm Chart,一把梭部署Prometheus、Alertmanager、Grafana和一堆exporter:
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install monitoring prometheus-community/kube-prometheus-stack \
--namespace monitoring \
--create-namespace \
--set prometheus.prometheusSpec.retention=15d \
--set prometheus.prometheusSpec.resources.requests.memory=8Gi \
--set prometheus.prometheusSpec.resources.limits.memory=32Gi \
--set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage=200Gi \
--set prometheus.prometheusSpec.replicas=2这里有几个参数说下为什么这么配:
retention=15d:本地只存15天,长期存储交给Thanos- 内存request 8G、limit 32G:Prometheus吃内存大户,给少了OOM很常见
replicas=2:双副本,一个挂了另一个顶上
label基数爆炸问题
跑了两周Prometheus内存飙到32G,查PromQL转圈。排查发现开发同学这么埋的点:
# 错误示范!user_id是高基数值,绝对不能当label
REQUEST_COUNTER.labels(
service="order-service",
method="POST",
endpoint="/api/order",
user_id=request.user_id, # 这行就是罪魁祸首
status_code=response.status
).inc()正确做法:
# label只用低基数枚举值
REQUEST_COUNTER.labels(
service="order-service",
method="POST",
endpoint="/api/order",
status_code=response.status
).inc()
# user_id这种高基数信息放到日志或trace里去怎么发现哪些指标有问题?用这个PromQL查时间线top10:
topk(10, count by (__name__)({__name__=~".+"}))或者用Prometheus自带的TSDB状态页面 /tsdb-status,能直接看到哪些label的基数最高。
我们砍掉不合理label后:时间线200万 → 40万,内存32G → 14G。
Thanos高可用部署
每个集群的Prometheus配Thanos Sidecar:
# prometheus的额外配置,开启Sidecar需要的external_labels
apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
name: k8s
namespace: monitoring
spec:
externalLabels:
cluster: cluster-beijing-01 # 每个集群必须有唯一标识
region: cn-north-1
thanos:
image: quay.io/thanos/thanos:v0.35.1
objectStorageConfig:
key: thanos-storage.yaml
name: thanos-objstore-config对象存储配置(S3兼容):
# thanos-storage.yaml
type: S3
config:
bucket: "prometheus-thanos-data"
endpoint: "s3.cn-north-1.amazonaws.com.cn"
access_key: "${AWS_ACCESS_KEY_ID}"
secret_key: "${AWS_SECRET_ACCESS_KEY}"Thanos Query做跨集群聚合查询:
apiVersion: apps/v1
kind: Deployment
metadata:
name: thanos-query
namespace: monitoring
spec:
replicas: 2
template:
spec:
containers:
- name: thanos-query
image: quay.io/thanos/thanos:v0.35.1
args:
- query
- --store=dnssrv+_grpc._tcp.thanos-sidecar-beijing.monitoring.svc
- --store=dnssrv+_grpc._tcp.thanos-sidecar-shanghai.monitoring.svc
- --store=dnssrv+_grpc._tcp.thanos-sidecar-guangzhou.monitoring.svc
- --store=dnssrv+_grpc._tcp.thanos-store-gateway.monitoring.svc
- --query.replica-label=prometheus_replicaCompactor的坑——降低并发防OOM:
thanos compact \
--data-dir=/tmp/thanos-compact \
--objstore.config-file=/etc/thanos/storage.yaml \
--retention.resolution-raw=14d \
--retention.resolution-5m=30d \
--retention.resolution-1h=90d \
--block-sync-concurrency=2 \ # 默认20太高,16G内存扛不住
--compact.concurrency=1Loki部署和优化
Simple Scalable模式部署
helm repo add grafana https://grafana.github.io/helm-charts
helm install loki grafana/loki \
--namespace monitoring \
--set deploymentMode=SimpleScalable \
--set loki.storage.type=s3 \
--set loki.storage.s3.endpoint=s3.cn-north-1.amazonaws.com.cn \
--set loki.storage.s3.bucketnames=loki-logs-data \
--set loki.storage.s3.region=cn-north-1 \
--set loki.schemaConfig.configs[0].from="2024-01-01" \
--set loki.schemaConfig.configs[0].store=tsdb \
--set loki.schemaConfig.configs[0].object_store=s3 \
--set loki.schemaConfig.configs[0].schema=v13 \
--set loki.schemaConfig.configs[0].index.prefix=loki_index_ \
--set loki.schemaConfig.configs[0].index.period=24h关键性能配置
Loki的values.yaml里这些参数很关键:
loki:
limits_config:
max_query_series: 5000 # 限制单查询返回的series数
max_query_parallelism: 32 # 查询并发度
max_entries_limit_per_query: 10000
split_queries_by_interval: 1h # 大查询拆分成1h粒度并行
query_timeout: 5m
chunk_store_config:
chunk_cache_config:
embedded_cache:
enabled: true
max_size_mb: 2048 # 给chunk缓存分2G内存
ingester:
chunk_encoding: snappy # 换成snappy,写入性能+30%
chunk_target_size: 1572864 # 1.5MB,比默认值大Alloy采集配置
// alloy配置 - 采集K8s容器日志
discovery.kubernetes "pods" {
role = "pod"
}
discovery.relabel "pod_logs" {
targets = discovery.kubernetes.pods.targets
rule {
source_labels = ["__meta_kubernetes_namespace"]
target_label = "namespace"
}
rule {
source_labels = ["__meta_kubernetes_pod_name"]
target_label = "pod"
}
rule {
source_labels = ["__meta_kubernetes_pod_container_name"]
target_label = "container"
}
rule {
source_labels = ["__meta_kubernetes_pod_label_app"]
target_label = "app"
}
}
loki.source.kubernetes "pods" {
targets = discovery.relabel.pod_logs.output
forward_to = [loki.process.pipeline.receiver]
}
loki.process "pipeline" {
// 提取JSON日志中的level字段作为label
stage.json {
expressions = { level = "level" }
}
stage.labels {
values = { level = "" }
}
// 丢弃debug级别日志,减少存储
stage.drop {
expression = ".*"
source = "level"
value = "debug"
}
forward_to = [loki.write.default.receiver]
}
loki.write "default" {
endpoint {
url = "http://loki-gateway.monitoring.svc:3100/loki/api/v1/push"
}
}告警治理:完整配置
Alertmanager路由配置
# alertmanager-config.yaml
global:
resolve_timeout: 5m
route:
receiver: 'default-webhook'
group_by: ['alertname', 'cluster', 'namespace']
group_wait: 30s # 同一组告警等30s再发,合并通知
group_interval: 5m # 同一组5分钟内有新告警才重新发
repeat_interval: 4h # 同一条告警4小时重复一次
routes:
- match:
severity: critical
receiver: 'phone-call'
group_wait: 10s # P0级别等10s就发
repeat_interval: 10m
- match:
severity: warning
receiver: 'wechat-oncall'
- match:
severity: info
receiver: 'wechat-channel'
# 抑制规则:底层告警抑制上层衍生告警
inhibit_rules:
# 节点挂了,抑制该节点上所有pod告警
- source_match:
alertname: NodeDown
target_match_re:
alertname: PodCrashLooping|PodNotReady|ContainerOOMKilled
equal: ['node']
# 集群级别故障抑制服务级别告警
- source_match:
severity: critical
scope: cluster
target_match:
severity: warning
equal: ['cluster']
receivers:
- name: 'phone-call'
webhook_configs:
- url: 'http://alert-gateway.monitoring.svc:8080/api/v1/phone'
send_resolved: true
- name: 'wechat-oncall'
webhook_configs:
- url: 'http://alert-gateway.monitoring.svc:8080/api/v1/wechat/oncall'
send_resolved: true
- name: 'wechat-channel'
webhook_configs:
- url: 'http://alert-gateway.monitoring.svc:8080/api/v1/wechat/channel'
- name: 'default-webhook'
webhook_configs:
- url: 'http://alert-gateway.monitoring.svc:8080/api/v1/wechat/default'几条核心告警规则
# prometheus-rules.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: core-alerts
namespace: monitoring
spec:
groups:
- name: service-sla
rules:
# 错误率超5%持续2分钟
- alert: HighErrorRate
expr: |
(
sum(rate(http_requests_total{status=~"5.."}[5m])) by (namespace, service)
/
sum(rate(http_requests_total[5m])) by (namespace, service)
) > 0.05
for: 2m
labels:
severity: critical
annotations:
summary: "{{ $labels.service }} 错误率 {{ $value | humanizePercentage }}"
# P99延迟超过2秒
- alert: HighLatencyP99
expr: |
histogram_quantile(0.99,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, namespace, service)
) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "{{ $labels.service }} P99延迟 {{ $value }}s"
- name: infrastructure
rules:
# 节点内存使用率超90%
- alert: NodeMemoryHigh
expr: |
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) > 0.9
for: 5m
labels:
severity: warning
annotations:
summary: "节点 {{ $labels.instance }} 内存使用率 {{ $value | humanizePercentage }}"
# Pod频繁重启
- alert: PodCrashLooping
expr: |
increase(kube_pod_container_status_restarts_total[1h]) > 5
for: 5m
labels:
severity: warning
annotations:
summary: "{{ $labels.namespace }}/{{ $labels.pod }} 1小时内重启 {{ $value }} 次"
- name: watchdog
rules:
# 监控系统自身的存活探测
- alert: Watchdog
expr: vector(1)
labels:
severity: none
annotations:
summary: "Watchdog正常,如果这条告警消失说明Alertmanager挂了"告警自愈webhook示例
# alert_handler.py - 接收Alertmanager webhook,执行自愈
from flask import Flask, request
import subprocess
import json
app = Flask(__name__)
@app.route('/api/v1/auto-heal', methods=['POST'])
def auto_heal():
data = request.json
for alert in data.get('alerts', []):
alert_name = alert['labels'].get('alertname')
namespace = alert['labels'].get('namespace')
pod = alert['labels'].get('pod')
if alert_name == 'PodCrashLooping' and alert['status'] == 'firing':
# OOM频繁重启的pod,尝试重启deployment
deploy = pod.rsplit('-', 2)[0] # 从pod名推断deployment
cmd = f"kubectl rollout restart deployment/{deploy} -n {namespace}"
result = subprocess.run(cmd.split(), capture_output=True, timeout=30)
if result.returncode != 0:
# 自愈失败,通知人工介入
notify_oncall(f"自愈失败: {namespace}/{deploy}, 需人工处理")
elif alert_name == 'DiskSpaceHigh':
node = alert['labels'].get('instance')
# 清理Docker镜像缓存
cmd = f"ssh {node} docker system prune -f --filter until=72h"
subprocess.run(cmd.split(), capture_output=True, timeout=60)
return json.dumps({"status": "ok"}), 200Grafana Dashboard配置要点
Dashboard按四层组织,用变量实现模板化:
// Dashboard变量配置示例
{
"templating": {
"list": [
{
"name": "namespace",
"type": "query",
"query": "label_values(kube_pod_info, namespace)",
"refresh": 2
},
{
"name": "service",
"type": "query",
"query": "label_values(http_requests_total{namespace=\"$namespace\"}, service)",
"refresh": 2
}
]
}
}从指标跳转到日志的关键——Grafana Data Source配置里设置Derived Fields:
# Grafana Loki数据源配置
derivedFields:
- name: TraceID
matcherRegex: "traceID=(\\w+)"
url: "http://tempo.monitoring.svc:3200/api/traces/${__value.raw}"
datasourceUid: tempo-datasource最终成果
跑了3个多月,硬数据:
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 月度监控成本 | 1.2万 | ~2000(机器+存储) |
| 日均告警数 | 200+ | 5条 |
| 故障发现到响应 | 15-22分钟 | 3分钟内 |
| 日志查询P99 | 超时/15s+ | 3s内 |
| 监控覆盖率 | ~60%服务 | 100%服务+中间件 |
整套系统资源占用:约60C100G(Prometheus双副本各16C32G + Thanos 8C16G + Loki三组件各4C8G + Grafana 2C4G)。
成本降了80%,灵活度和故障响应效率都上了一个台阶。
几条可复用的架构准则
- 监控不是越多越好,是越准越好。200条告警没人看 = 没有监控。
- label设计决定了Prometheus和Loki 80%的使用体验,这个必须在接入前就规范好。
- 告警分级+抑制规则是告警体系的灵魂,没有这两样,告警就是噪音制造机。
- 先跑通核心链路再逐步完善——一步到位铺太大,很容易搞到一半没人维护。
- 监控系统本身需要有独立的存活检测机制,不能只有一条监控链路。
你们公司的监控体系现在是什么状态?是还在用云厂商服务,还是已经自建了?自建过程中踩过什么坑,评论区聊聊,互相避雷。
觉得有用的话帮忙点个转发,让更多人少走弯路。关注我,后续还会持续分享云原生和SRE方向的实战干货。
公众号:耕云躬行录
个人博客:躬行笔记