监控告警体系设计实践:从 ELK 到 Prometheus
大家好,我是云萱。
监控和告警是 DevOps 的基本功,但很多团队的监控体系都有问题——要么是告警太多没人看,要么是出了问题告警没响。“监控告警”四个字说起来简单,做好其实不容易。
这篇文章把我整理的监控告警相关知识分享出来,从架构设计到告警策略,再到实战中的日志监控机器人,尽量覆盖全面。
一、可观测性的三大支柱
可观测性(Observability)不是只有监控,它有三大支柱:
| 作用 | 典型工具 | 数据特点 | |
|---|---|---|---|
| Logging(日志) | 记录具体事件,用于排查问题 | ELK、Loki | 非结构化/半结构化,量大 |
| Metrics(指标) | 聚合统计,用于监控告警 | Prometheus、InfluxDB | 结构化,数值型,相对量小 |
| Tracing(链路追踪) | 请求全链路追踪,性能分析 | Jaeger、Zipkin、SkyWalking | 结构化,带 trace ID |
三者是互补关系,不是替代关系。
排查问题的典型路径:
告警触发(Metrics)
→ 定位到某个服务/接口(Tracing)
→ 查看具体错误日志(Logging)
很多团队一开始只做了日志和指标,忽略了链路追踪。等服务多了之后,排查问题就像大海捞针——你知道有问题,但不知道是哪个环节出的。
二、日志系统架构(ELK)
经典 ELK 架构
应用 → Filebeat → Logstash → Elasticsearch → Kibana
↓(可选)
Kafka / Redis(缓冲层,防止日志洪峰)
各组件作用:
- Filebeat:轻量日志采集器,部署在每台机器上,收集日志发往 Logstash 或 ES
- Logstash:日志处理管道,过滤、解析、转换日志格式
- Elasticsearch:分布式搜索引擎,存储和索引日志
- Kibana:可视化界面,搜索、分析、仪表盘
- Kafka / Redis(可选):缓冲层,当日志量很大的时候用,防止 ES 被打垮
日志采集最佳实践
1. 结构化日志优先 尽量输出 JSON 格式的日志,而不是纯文本。结构化日志查询和统计起来方便太多了。
不好的格式:
[2024-01-01 12:00:00] ERROR user-service login failed user=123 ip=1.2.3.4 reason=wrong_password
好的格式:
{
"timestamp": "2024-01-01T12:00:00Z",
"level": "ERROR",
"service": "user-service",
"action": "login",
"user_id": 123,
"client_ip": "1.2.3.4",
"reason": "wrong_password"
}
2. 统一日志格式 所有服务的日志字段要统一(时间、级别、服务名、trace_id 等),不然后面查日志的时候很痛苦。
3. 日志分级
- DEBUG:开发调试用,生产环境关闭
- INFO:正常业务日志
- WARN:警告,不影响运行但需要关注
- ERROR:错误,需要处理
- FATAL:致命错误,服务不可用
4. 不要打敏感信息 密码、身份证、银行卡号这些,打日志前一定要脱敏。
ES 索引模板设计
建议按日期分索引(比如 app-logs-2024.01.01),方便管理和清理。
索引模板里要注意:
- 关键字段(level、service、trace_id)设为 keyword 类型,方便聚合
- message 字段用 text + keyword 双类型(既可以全文检索又可以聚合)
- 设置合理的分片数和副本数
- 配合 ILM(索引生命周期管理)自动滚动和删除过期索引
三、指标体系(Prometheus)
四种指标类型
| 类型 | 含义 | 例子 |
|---|---|---|
| Counter | 只增不减的计数器 | 请求总数、错误数、处理字节数 |
| Gauge | 可增可减的瞬时值 | CPU 使用率、内存使用率、队列长度 |
| Histogram | 直方图,统计分布 | 请求延迟分布、响应大小分布 |
| Summary | 类似 Histogram,直接计算分位数 | 客户端计算分位数 |
面试常考:Histogram 和 Summary 的区别?
- Histogram 在服务端统计分位数,占存储空间,但可以聚合
- Summary 在客户端统计分位数,省存储空间,但不能聚合
- 大多数场景推荐用 Histogram
黄金指标(Four Golden Signals)
Google SRE 提出的四个核心监控指标:
- 延迟(Latency) - 处理请求花了多久
- 流量(Traffic) - 每秒多少请求
- 错误率(Errors) - 失败请求占比
- 饱和度(Saturation) - 资源使用了多少(CPU、内存、磁盘)
把这四个指标监控到位,系统的健康状况基本就有数了。
RED 方法(微服务场景)
针对微服务的每一个服务,监控三个指标:
- Rate:请求速率
- Errors:错误率
- Duration:请求耗时
比 USE 方法(使用率/饱和度/错误,适合资源监控)更适合服务监控。
四、告警体系设计
很多团队的告警是”拍脑袋”加的——看到一个指标就加个告警。结果告警泛滥,大家都麻木了,真正重要的告警反而被淹没。
告警分级策略
我推荐 P0-P3 四级告警:
| 级别 | 响应时间 | 通知方式 | 示例 | 处理原则 |
|---|---|---|---|---|
| P0 - 紧急 | 立即(5 分钟内) | 电话 + 短信 + IM | 整个系统不可用、数据丢失 | 立即处理,不分昼夜 |
| P1 - 严重 | 15 分钟内 | IM + 电话 | 核心功能不可用、大量用户影响 | 工作时间立即,非工作时间尽快 |
| P2 - 一般 | 1 小时内 | IM | 非核心功能异常、性能下降 | 工作时间处理 |
| P3 - 提示 | 工作日处理 | 邮件 / 工单 | 磁盘使用率预警、日志告警 | 有空再看 |
原则:告警级别要和实际影响匹配。 P0/P1 的告警必须”少而准”——每次响都是真的有大事。如果一周响好几个 P1 都是鸡毛蒜皮的事,下次真出大事就没人信了。
避免告警风暴
告警风暴是最常见的问题。几个解决思路:
1. 告警抑制(Deduplication) 同一类告警,短时间内只发一次。比如同一个错误 1 分钟内出现 100 次,只发 1 次告警。
2. 告警聚合(Aggregation) 同一服务的多个告警聚合成一条。比如一个服务的 10 个 Pod 同时挂了,发一条”服务 X 有 10 个 Pod 不可用”的告警,而不是 10 条。
3. 告警静默(Silence) 已知的维护窗口,提前设置静默,避免骚扰。
4. 告警分级 前面说的 P0-P3,不同级别不同处理方式。P3 的告警不应该半夜把人叫起来。
5. 自动恢复通知 故障恢复了也要通知。不然你爬起来处理了半天,结果人家自己好了。
告警设计原则
- 告警必须可行动:收到告警之后要知道”该做什么”。如果一个告警收到之后只能”先看看”,那这个告警就不应该存在,或者级别不够。
- 告警要有 owner:每个告警都要明确谁负责。没人负责的告警 = 没人管的告警。
- 定期清理失效告警:系统改了,告警也要跟着改。不然过半年告警列表里一半都是没用的。
五、实战:Python 日志监控机器人
光有 ELK 还不够——有时候你需要对特定日志做实时监控和告警。我用 Python 写过一个日志监控机器人,分享一下思路。
核心功能
- 实时监控日志文件(类似 tail -f)
- 多规则匹配(关键词、正则、日志级别)
- 告警去重与抑制(防止告警风暴)
- 多通道推送(企业微信、钉钉、飞书、邮件)
- 支持从 Elasticsearch 查询历史日志
关键模块设计
1. 告警去重器
核心思路:对同一种告警(规则名 + 相似内容),在一个时间窗口内只发 N 次。
class AlertDeduplicator:
def __init__(self, dedup_window=300, max_alerts=5):
self.dedup_window = dedup_window # 去重窗口(秒)
self.max_alerts = max_alerts # 窗口内最多告警次数
self.alert_history = defaultdict(deque)
def should_alert(self, rule_name, content):
fp = self._fingerprint(rule_name, content)
now = time.time()
cutoff = now - self.dedup_window
# 清理过期记录
history = self.alert_history[fp]
while history and history[0] < cutoff:
history.popleft()
if len(history) < self.max_alerts:
history.append(now)
return True, len(history)
else:
return False, len(history)
2. 日志解析器
支持多种日志格式(nginx、java、通用等),用正则把非结构化日志解析成结构化数据。
3. tail -f 实现原理
# 核心就是:定期检查文件大小和 inode
while True:
line = f.readline()
if line:
process(line)
else:
time.sleep(1)
# 检查文件是否被轮转了(inode 变了)
new_inode = os.stat(filename).st_ino
if new_inode != current_inode:
f.close()
f = open(filename)
current_inode = new_inode
4. 告警分级推送
不同级别走不同通道:
- P0:电话 + 短信 + IM
- P1:IM + 电话
- P2:IM
- P3:邮件/工单
完整的脚本在我的 DevOps 面试脚本合集中,感兴趣的可以去看
09-python/log-monitor.py。
六、Prometheus 告警规则示例
给大家看一组实际生产中用的告警规则,按类别分:
系统级别(主机)
groups:
- name: system
rules:
- alert: HostHighCPU
expr: 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
for: 5m
labels:
severity: P2
annotations:
summary: "主机 CPU 使用率过高"
description: "{{ $labels.instance }} CPU 使用率 {{ $value | printf \"%.2f\" }}%"
- alert: HostHighMemory
expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 85
for: 5m
labels:
severity: P2
- alert: HostDiskAlmostFull
expr: (1 - node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes) * 100 > 85
for: 5m
labels:
severity: P1
应用级别
- name: application
rules:
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.05
for: 2m
labels:
severity: P1
annotations:
summary: "服务错误率过高"
description: "{{ $labels.service }} 错误率 {{ $value | printf \"%.2f\" }}%"
- alert: HighLatencyP95
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 3
for: 5m
labels:
severity: P2
- alert: PodCrashLooping
expr: rate(kube_pod_container_status_restarts_total[15m]) * 60 * 15 > 3
for: 0m
labels:
severity: P1
Kubernetes 级别
- name: kubernetes
rules:
- alert: NodeNotReady
expr: kube_node_status_condition{condition="Ready",status="true"} == 0
for: 5m
labels:
severity: P1
- alert: DeploymentNotAvailable
expr: kube_deployment_status_available_replicas < kube_deployment_spec_replicas
for: 10m
labels:
severity: P2
数据库级别
- name: database
rules:
- alert: MySQLDown
expr: mysql_up == 0
for: 1m
labels:
severity: P0
- alert: MySQLSlowQueries
expr: rate(mysql_slow_queries_total[5m]) > 10
for: 5m
labels:
severity: P2
- alert: MySQLReplicationLag
expr: mysql_slave_status_seconds_behind_master > 60
for: 2m
labels:
severity: P1
七、面试中常见问题
Q: 你们的监控体系是怎么设计的?
按照三大支柱来设计:
- Metrics 用 Prometheus + Grafana,做指标监控和告警
- Logging 用 Filebeat + Kafka + Logstash + Elasticsearch + Kibana,做日志搜索分析
- Tracing 用 Jaeger,做分布式链路追踪
告警用 Alertmanager,做告警分级、去重、聚合、路由。
Q: 告警风暴怎么处理?
- 告警分级(P0-P3),不同级别不同响应
- 告警去重:同一窗口内相同告警只发几次
- 告警聚合:同一服务的多个问题聚合成一条
- 告警静默:维护期间提前静默
- 精简告警:只保留可行动的告警,没用的都删掉
Q: ELK 性能优化有哪些?
- 写入端:使用 Kafka 做缓冲,批量写入,用 SSD
- 索引:按日期分索引,合理设置分片数,mapping 设计好(不要的字段不索引)
- 查询:尽量用 filter 上下文(走缓存),避免大范围查询
- 集群:节点角色分离(master/data/ingest),冷热分层存储
- 生命周期管理:ILM 自动滚动、收缩、删除
Q: Prometheus 怎么实现高可用?
几种方案:
- 联邦集群(Federation):多个 Prometheus 实例,上层聚合
- 主从复制:Thanos / VictoriaMetrics
- 直接用 VictoriaMetrics 替代 Prometheus(兼容 PromQL,性能更好)
Alertmanager 本身就是高可用的,多个实例之间通过 gossip 同步告警状态。
写在最后
监控告警这个事,核心不是”装了多少工具”,而是”出了问题能不能及时发现、快速定位”。
我见过太多团队,监控工具一大堆,告警满天飞,真出了故障还是靠人肉排查。这样的监控体系是失败的。
好的监控体系应该是:
- 少而准的告警 - 每次响都值得爬起来处理
- 清晰的排查路径 - 从告警到根因的路径短
- 完善的仪表盘 - 看一眼就知道系统状态
- 持续迭代优化 - 每次故障后复盘,补监控、加告警、优化流程
监控不是一次性的事,是要持续打磨的。
希望这篇文章对你有帮助~