2026-04-22 · architecture / observability
【可观测性工程】可观测性全景:Metrics、Logs、Traces、Profiles、Events 五大支柱
从控制论到云原生:拆解可观测性的五大信号支柱,对比监控与可观测性的本质区别,梳理开源/商业/SaaS 分类,以及国内互联网公司三大支柱落地现状与典型工程坑点。





















在很多团队的工程语境里,“监控”和”可观测性”被当作同义词使用:有同事把 Grafana 仪表盘叫做”监控大盘”,把 Prometheus 告警叫做”监控告警”,把 Jaeger 里的 Trace 视图也叫做”监控链路”。这种用法在日常沟通中无伤大雅,但它掩盖了一个在架构层面非常关键的事实——监控(Monitoring)和可观测性(Observability)不是一个东西的两个名字,它们来自完全不同的认知模型,对应完全不同的数据组织方式,解决完全不同的问题。
把这两个概念混用的代价,会在系统规模上去、故障类型变得”离奇”之后显现出来。一个典型的症状是:监控面板上所有指标全部绿色,但用户已经在投诉;告警没有响,但 SLO 已经破了;值班同事盯着十几个仪表盘逐个切换,却找不到故障根因。这时候人们才会意识到,自己搭的其实是一套”监控系统”,而不是一套”可观测性系统”。
本文尝试把这段历史捋清楚。从 1999 年 Nagios 1.0 发布,到 2019 年 OpenTelemetry 合并成立,再到 2023 年 OpenTelemetry Metrics 与 Logs 正式 GA,整整二十四年时间里,行业在这件事上反复试错了至少五轮。每一轮的设计选择——推(push)还是拉(pull)、层级命名还是标签化、文本协议还是 gRPC、单一数据模型还是三支柱统一——都有非常具体的历史原因,也都留下了非常具体的工程坑点。理解这段历史,比盲目追新工具更重要。
监控的核心范式可以用一个最小环路来概括:预设问题 → 收集指标 → 阈值触发 → 人工响应。这个环路的起点是”预设问题”,也就是说,在系统上线之前,运维工程师必须已经能够列出所有他关心的指标,才能针对每个指标配置采集脚本和告警阈值。
举个最经典的例子:一个 Web 服务器,运维会预设如下几个问题:
pidof 或 TCP
探测回答)df -h 回答)uptime 或
/proc/loadavg 回答)每一个问题都对应一个脚本,每一个脚本的输出都对应一个数值,每一个数值都对应一个阈值。只要系统在预设范围内运行,监控就能工作得很好。
这套范式最成熟的实现是 Nagios。Nagios 1.0 由 Ethan Galstad 于 1999 年发布,它把”预设问题”变成了工程意义上的”插件”。Nagios 的核心只做三件事:调度检查、记录状态、发送通知。真正的检查逻辑全部由外部插件实现,每个插件就是一个可执行文件,约定输入参数、约定退出码(0=OK、1=WARNING、2=CRITICAL、3=UNKNOWN)、约定标准输出格式。
典型的 Nagios 插件包括:
check_http:用 HTTP 请求探测网站check_disk:用 statvfs
查磁盘使用率check_load:读
/proc/loadavgcheck_procs:统计符合条件的进程数check_tcp:TCP 端口连通性check_ping:ICMP ping这种”一切皆插件”的设计让 Nagios 在社区里繁荣了十多年,Monitoring Plugins 项目(原 Nagios Plugins)至今仍在维护数百个官方插件,覆盖几乎所有常见服务。
对于无法直接从 Nagios 主机探测的场景,Nagios 提供了两种方案:一种是 NRPE(Nagios Remote Plugin Executor),它在被监控机器上运行一个守护进程,Nagios 主机通过 TLS 连接过去远程触发插件执行;另一种是 NSCA(Nagios Service Check Acceptor),被监控主机定期把结果”推”到 Nagios 主机,常用于防火墙隔离场景。
一份极简的 Nagios 主配置大致长这样:
# nagios.cfg(节选)
log_file=/var/log/nagios/nagios.log
cfg_dir=/etc/nagios/objects
status_file=/var/log/nagios/status.dat
command_check_interval=-1
service_check_timeout=60
host_check_timeout=30
enable_notifications=1
execute_service_checks=1
accept_passive_service_checks=1
use_large_installation_tweaks=1对应的服务定义:
# services.cfg
define service {
use generic-service
host_name web01.example.com
service_description HTTP
check_command check_http!-H www.example.com -u / -s "200 OK" -w 3 -c 5
max_check_attempts 3
check_interval 1
retry_interval 1
check_period 24x7
notification_interval 30
notification_period 24x7
contact_groups web-admins
}
define service {
use generic-service
host_name web01.example.com
service_description Disk /var
check_command check_nrpe!check_disk_var
max_check_attempts 3
check_interval 5
}以及 NRPE 在远端的配置:
# /etc/nagios/nrpe.cfg
allowed_hosts=10.0.0.1
dont_blame_nrpe=0
command[check_disk_var]=/usr/lib/nagios/plugins/check_disk -w 20% -c 10% -p /var
command[check_load]=/usr/lib/nagios/plugins/check_load -w 5,4,3 -c 10,8,6
command[check_procs_nginx]=/usr/lib/nagios/plugins/check_procs -C nginx -c 1:这份配置本质上是在做一件事:把每一个”我关心的问题”翻译成一次周期性的命令执行和一次阈值比较。
传统监控的告警基本可以归为两大类:
存活检测(Liveness Check):只关心”在不在”。端口能不能连上?进程是不是还存在?HTTP 能不能返回 2xx?这类检查的输出是布尔型的,只有 UP / DOWN 两种状态。它处理的是最基础的可用性问题——当一台机器宕机、一个进程崩溃、一个网络分区形成时,存活检测能在分钟级别发现并通知。
阈值告警(Threshold Alert):关心”量”。CPU 超过 80% 告警、磁盘剩余少于 10% 告警、每分钟请求量少于 100 告警。阈值告警的本质是把一个连续变量(指标值)离散化成几个状态(OK / WARNING / CRITICAL)。
两类告警合起来能覆盖绝大多数”系统层”问题。在单体应用时代、物理机或小规模虚拟机时代,这套机制工作得相当好。
但是这种范式有一个根本性的限制。Donald Rumsfeld 在 2002 年留下过一段现在被反复引用的话:
“There are known knowns; there are things we know we know. We also know there are known unknowns; that is to say, we know there are some things we do not know. But there are also unknown unknowns—the ones we don’t know we don’t know.”
翻译成中文大致是:
传统监控能覆盖的范围是”已知的未知”——我知道 CPU 会影响性能(这是”已知”),所以我监控 CPU 使用率(用一个指标填充”未知”的取值)。当我想监控 CPU 时,我的认知里必须已经存在”CPU 使用率会影响系统行为”这个假设。
问题在于:在真实的生产故障里,“未知的未知”占了绝对大多数。比如:
0
开头时走了错误的分支,只对占总量 1.1% 的商品生效这些故障的共同特征是:事前没有人会为它们配置监控,因为没有人会想到要配置。监控系统天然只能回答”你已经想到的问题”——这不是工具的 bug,是架构层面的约束。
换句话说,监控的认知上限等于设计者在系统上线前能想到的问题集合。这个集合是有限的、静态的,而生产系统的故障空间是无限的、动态的。当系统复杂到一定程度(微服务、分布式、多租户、异构),你事前能枚举的问题和实际会发生的问题之间的差距会呈指数级拉大。
这就是为什么在 2010 年代后期,随着微服务架构普及,传统监控开始变得越来越力不从心。工程师发现自己配了成百上千条告警规则,但一出事还是只能靠猜。
“可观测性”这个词并不是云原生时代发明的。它来自 Rudolf Kálmán(卡尔曼滤波器的那个 Kálmán)1960 年在控制论中给出的严格定义:
一个系统是可观测的(observable),当且仅当可以通过其输出(output)的有限次观测推断出其内部状态(internal state)。
在控制论语境下,这是一个线性代数问题——看观测矩阵的秩。拿到软件工程领域后,Charity Majors 给出了工程化的翻译:
可观测性是指你能够回答关于系统内部状态的任意问题,而无需事先知道你要问什么问题,也无需发布新代码来收集额外的遥测数据。
这个定义里有两个关键限定:
很多团队把”可观测性”理解成”更多的监控”——上更多的仪表盘、配更多的告警规则、接入更多的指标。这种理解是错的。
可观测性的核心不是”量”,是”维度”。如果你采集了一百个指标,但每个指标只有”服务名”一个标签,那么无论这一百个指标的数量有多大,你能回答的问题仍然是静态的一组——你永远无法用这些数据回答”某个特定用户的请求为什么变慢了”。
可观测性需要的是:在系统的每一个关键事件上,记录足够多的上下文维度。当你记录的维度足够多时,事后可以用任意维度的组合去切分和聚合数据。这才是”任意问题可回答”的真正含义。
这就引出了两个工程上的关键概念:
高基数(High
Cardinality):一个字段的取值空间很大。user_id
是典型的高基数字段——几千万用户每人一个值。request_id
基数更高,每个请求一个唯一值。
高维度(High Dimensionality):事件上附带的字段数量多。一个 HTTP 请求的事件,可能带有 user_id、region、device、app_version、route、status_code、response_bytes、upstream、ab_bucket、…… 共几十个字段。
传统监控系统(Nagios、Zabbix、Graphite)都是反高基数的,因为它们的存储模型本质上是”每个标签组合一条时间序列”,基数高了以后序列数量爆炸,索引和查询都会崩掉。Prometheus 部分缓解了这个问题,但也有著名的”label cardinality 爆炸”踩坑史。
真正高基数友好的是事件型存储(Event-oriented storage),比如 Honeycomb 的 columnar event store,或者基于日志/追踪的宽事件(wide event)方案。
再举一个例子说明维度的重要性:
假设一个服务总共 10000 QPS,平均延迟 50 ms。看起来很健康。但如果你把数据按 user_id 切开,发现其中 10 QPS(万分之一)的平均延迟是 5000 ms,这 10 QPS 全部来自某个大客户,而这个大客户刚刚签了八位数的合同——你的”平均延迟 50 ms”实际上已经是一个严重事故。
如果你只记录”平均延迟”这一个标量,永远发现不了这种问题。如果你记录了分布(p50/p95/p99/p999),能发现”有人慢”;如果你记录了分布 + user_id 维度,才能发现”是谁慢”。
所以可观测性的工程落地,本质上是在回答一个问题:如何在可承受的存储和查询成本内,保留足够多的事件维度。这是整个领域过去十年的主线,也是 OpenTelemetry 设计的主要驱动力。
下面按时间段梳理整个演进过程。每一段都有它自己的时代背景,也都有解决和未解决的问题。
1999 年,Ethan Galstad 在自己的车库里写出了 NetSaint,后来因为商标原因改名为 Nagios(“Nagios Ain’t Gonna Insist On Sainthood”,一个递归缩写)。这是服务器监控工具的事实标准的起点。
当时的时代背景是:
Nagios 的设计完美契合这个时代:
2001 年,Alexei Vladishev 在拉脱维亚发布了 Zabbix。Zabbix 和 Nagios 解决的问题类似,但路线不同——Zabbix 更强调”一站式”,自带数据库(MySQL/PostgreSQL)、自带 Web 界面、自带图表,而 Nagios 的生态是靠一堆插件拼起来的(比如 NagVis、PNP4Nagios、Nagiosgraph、Thruk)。
Zabbix 的另一个核心设计是”Zabbix Agent”——每台被监控主机装一个 agent,主动采集本地指标,通过 TCP 上报给 Zabbix Server。Zabbix 从一开始就支持主动模式(active)和被动模式(passive):
一段典型的 Zabbix Agent 配置:
# zabbix_agentd.conf
Server=10.0.0.1
ServerActive=10.0.0.1
Hostname=web01.example.com
StartAgents=3
HostMetadataItem=system.uname
ListenPort=10050
LogFile=/var/log/zabbix/zabbix_agentd.log
LogFileSize=10
EnableRemoteCommands=0
UnsafeUserParameters=0
UserParameter=mysql.ping,mysqladmin ping | grep -c alive
UserParameter=nginx.conns[*],curl -s http://localhost/nginx_status | awk 'NR==3{print $$3}'Zabbix 的强大之处在于它的”模板(Template)“体系。一个模板把一组 item、trigger、graph、discovery rule 封装起来,应用到一组主机上。模板可以从 XML 导入导出,社区有大量现成模板(Linux、Nginx、MySQL、PostgreSQL、Redis、MongoDB、Java JMX、SNMP 设备等等)。
一段极简的 Zabbix 模板 XML 片段(示意):
<zabbix_export>
<version>5.0</version>
<templates>
<template>
<template>Template App Nginx</template>
<name>Template App Nginx</name>
<items>
<item>
<name>Nginx active connections</name>
<key>nginx.conns[active]</key>
<type>0</type>
<value_type>3</value_type>
<delay>30s</delay>
<history>7d</history>
<trends>365d</trends>
</item>
</items>
<triggers>
<trigger>
<expression>{Template App Nginx:nginx.conns[active].last()}>5000</expression>
<name>Too many active connections on {HOST.NAME}</name>
<priority>3</priority>
</trigger>
</triggers>
</template>
</templates>
</zabbix_export>在中国,Zabbix 从 2010 年前后开始被大量互联网公司采用,成为运维部门的事实标准。早期新浪、搜狐、京东、去哪儿等公司的 OP 团队基本都搭过 Zabbix。当时的典型架构是 Zabbix Server + MySQL + Zabbix Proxy,规模大的公司一台 Server 带几万到十几万的监控项(Item),超过这个数字通常要做分区或者分成多套。
这个时代的局限现在看得很清楚:
2006 年,Chris Davis 在 Orbitz 写了 Graphite,后来捐献给开源社区。Graphite 是第一个被广泛使用的”纯时序指标”系统。它的核心组件是:
path value timestamp\nGraphite 的指标命名是层级(hierarchical)的,用点号分隔:
servers.us-east-1.web01.cpu.idle
servers.us-east-1.web01.cpu.user
servers.us-east-1.web01.load.one_minute
services.checkout.qps
services.checkout.latency.p99
Carbon 接收协议之简单以至于你可以用一行 Python 或者甚至 netcat 推数据:
echo "servers.web01.cpu.idle 42.3 $(date +%s)" | nc -q0 graphite.example.com 20032011 年,Etsy 的 Ian Malpass 在一篇博客里介绍了他们内部开发的 StatsD。StatsD 是运行在应用旁边的 daemon,通过 UDP 接收应用发送的原始事件,聚合后推给 Graphite。
StatsD 的 UDP 协议格式是人类可读的:
# Counter:每次 +1
page.views:1|c
# Gauge:当前值
active_users:127|g
# Timer:一次耗时(毫秒)
login.latency:237|ms
# Set:去重计数
unique_ips:10.0.0.1|s
# Sampling:采样上报
api.calls:1|c|@0.1
StatsD 在应用侧聚合、批量 flush(通常 10 秒一次)给 Graphite。客户端侧因为是 UDP,“发完就忘”,对应用延迟影响几乎为零,即使 StatsD daemon 挂了应用也不会受影响。
这个时代还有几个值得一提的工具:
这个时代已经跟 Nagios 时代的思路完全不同了。Nagios 的核心是”状态(OK/WARNING/CRITICAL)“,时序指标时代的核心是”数值的历史”。告警不再是”一次检查的退出码”,而是”一段时间序列超过阈值”。
但时序指标时代的主要问题出在命名系统上。层级命名的根本缺陷是:维度写死在名字里。servers.us-east-1.web01.cpu.idle
里面包含了”us-east-1”、“web01”、“cpu.idle”三个维度信息,但它们只能从左到右展开,聚合困难。比如想问”所有机房的
cpu.idle 平均值”,在 Graphite 里需要用通配符函数:
averageSeries(servers.*.*.cpu.idle)
只要你想做稍微复杂一点的切片,比如”按机房聚合再取每个机房 95 分位”,Graphite 就很难表达。
Prometheus 的诞生直接针对上述问题。2012 年,Matt T. Proud 和 Julius Volz 在 SoundCloud(德国柏林的一家音乐流媒体公司)内部受 Google Borgmon 启发开始写 Prometheus。SoundCloud 当时面临的问题是:他们在从单体向微服务迁移,现有的 Graphite + StatsD 架构无法应付服务实例数量的爆炸。
Prometheus 的几个核心设计选择:
http_requests_total{method="GET", handler="/api/v1/users", status="200", instance="10.0.0.1:8080"}
拉取模型(Pull-based)。Prometheus Server 主动从目标抓取(scrape),而不是让目标推送过来。
服务发现(Service Discovery)。Prometheus 原生支持 Consul、Kubernetes、EC2、DNS SRV 等服务发现机制,目标列表动态变化。
自研查询语言 PromQL。表达聚合、过滤、函数运算非常自然。
本地 TSDB。不依赖外部数据库,每个 Prometheus 实例自己存 15 天的数据。
一份最小的 Prometheus 配置:
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
cluster: prod-east-1
rule_files:
- /etc/prometheus/rules/*.yml
alerting:
alertmanagers:
- static_configs:
- targets: ['alertmanager.example.com:9093']
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'node'
kubernetes_sd_configs:
- role: node
relabel_configs:
- source_labels: [__address__]
regex: '(.+):(.+)'
target_label: __address__
replacement: '${1}:9100'
- source_labels: [__meta_kubernetes_node_name]
target_label: instance
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
target_label: __address__
- action: labelmap
regex: __meta_kubernetes_pod_label_(.+)
- source_labels: [__meta_kubernetes_namespace]
action: replace
target_label: namespace
- source_labels: [__meta_kubernetes_pod_name]
action: replace
target_label: podPrometheus 2015 年 1 月正式发布 0.x 版本,2016 年加入 CNCF 成为第二个项目(第一个是 Kubernetes),2018 年 8 月毕业。
PromQL 的表达力是 Prometheus 最大的卖点之一。几个典型查询:
# 过去 5 分钟 5xx 错误率
sum by (service) (
rate(http_requests_total{status=~"5.."}[5m])
)
/
sum by (service) (
rate(http_requests_total[5m])
)
# 按实例和路径取 p99 延迟
histogram_quantile(0.99,
sum by (le, instance, handler) (
rate(http_request_duration_seconds_bucket[5m])
)
)
# 每个 Pod 的内存使用率
sum by (pod, namespace) (container_memory_working_set_bytes{container!="POD"})
/
sum by (pod, namespace) (kube_pod_container_resource_limits{resource="memory"})
# 告警:API 错误率超过 1%
(sum by (service) (rate(http_requests_total{status=~"5.."}[5m]))
/
sum by (service) (rate(http_requests_total[5m]))) > 0.01
对应的告警规则:
# rules/api.yml
groups:
- name: api.rules
interval: 30s
rules:
- alert: ApiHighErrorRate
expr: |
sum by (service) (rate(http_requests_total{status=~"5.."}[5m]))
/
sum by (service) (rate(http_requests_total[5m]))
> 0.01
for: 10m
labels:
severity: page
team: platform
annotations:
summary: "Service {{ $labels.service }} error rate {{ $value | humanizePercentage }}"
runbook: https://wiki.example.com/runbooks/api-high-error-rate
- alert: ApiHighLatency
expr: |
histogram_quantile(0.99,
sum by (le, service) (rate(http_request_duration_seconds_bucket[5m]))
) > 1
for: 10m
labels:
severity: ticketAlertmanager 负责告警的去重、分组、路由、静默:
# alertmanager.yml
route:
group_by: [alertname, cluster]
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receiver: default
routes:
- match:
severity: page
receiver: pagerduty
- match:
team: platform
receiver: platform-team
receivers:
- name: default
email_configs:
- to: oncall@example.com
- name: pagerduty
pagerduty_configs:
- routing_key: <key>
- name: platform-team
webhook_configs:
- url: https://hooks.slack.com/services/xxx/yyy/zzz
inhibit_rules:
- source_match:
severity: critical
target_match:
severity: warning
equal: [alertname, cluster]Prometheus 生态在 2016 年之后爆炸性增长。Exporter 成为行业标准——几乎任何你能想到的软件,都有对应的 exporter:node_exporter、mysqld_exporter、redis_exporter、blackbox_exporter、jmx_exporter、kafka_exporter、snmp_exporter、nginx-prometheus-exporter 等等。
2016 年同年,Grafana 2.x 开始支持 Prometheus 作为数据源,“Prometheus + Grafana”成为新的默认组合,取代了”Graphite + Graphite-Web”。
指标解决了”聚合状态”的问题,但对跨服务的单次请求链路追溯无能为力。这个空缺由分布式追踪(Distributed Tracing)填补。
历史上分布式追踪的源头是 Google 2010 年发表的 Dapper 论文。Twitter 受此启发开源了 Zipkin(2012);Uber 基于 Zipkin 的思想做了更大规模的 Jaeger(2017)。
但是每家公司的 SDK 都是自己的,埋点代码没法在不同系统间迁移,这促成了 OpenTracing 项目的诞生。OpenTracing 由 Ben Sigelman(Dapper 作者之一)于 2016 年发起,定义了一套语言无关的 API 规范:
典型代码(Python):
import opentracing
from opentracing.ext import tags
from opentracing.propagation import Format
tracer = opentracing.global_tracer()
def handle_request(request):
span_ctx = tracer.extract(Format.HTTP_HEADERS, request.headers)
with tracer.start_active_span('handle_request', child_of=span_ctx) as scope:
span = scope.span
span.set_tag(tags.HTTP_METHOD, request.method)
span.set_tag(tags.HTTP_URL, request.url)
span.set_tag('user.id', request.user.id)
result = do_work()
span.set_tag(tags.HTTP_STATUS_CODE, result.status)
return resultOpenTracing 只是规范,不是实现。真正的 Tracer 由 Jaeger、Zipkin 等项目各自提供兼容的实现。
2018 年,Google 开源了 OpenCensus,定位类似——但 OpenCensus 野心更大,它同时覆盖了 Metrics 和 Traces,而且是一个”库”而不仅是”规范”,开箱即用。OpenCensus 来自 Google 内部的 Census 库。
于是行业里出现了”双雄分裂”的局面:
| 维度 | OpenTracing | OpenCensus |
|---|---|---|
| 发起方 | LightStep 等 | |
| 覆盖范围 | 仅 Traces | Traces + Metrics |
| 定位 | API 规范 | API + SDK |
| 加入 CNCF | 2016 | 2019(孵化) |
两个项目的用户基数都不小,但谁也吃不掉对方,中间件作者要做选择题:到底实现哪个接口?用户也头疼:我用了 A,换库的时候发现中间件只支持 B。
2019 年 5 月,在 KubeCon Barcelona 上,两个项目的 Steering Committee 共同宣布合并为 OpenTelemetry(OTel)。OpenTelemetry 成为 CNCF 项目(现为活跃程度仅次于 Kubernetes 的第二大项目)。
OpenTelemetry 的组成:
一份 Collector 配置长这样:
# otel-collector.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
prometheus:
config:
scrape_configs:
- job_name: 'kube-state-metrics'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app]
regex: kube-state-metrics
action: keep
jaeger:
protocols:
thrift_http:
endpoint: 0.0.0.0:14268
zipkin:
endpoint: 0.0.0.0:9411
processors:
batch:
timeout: 10s
send_batch_size: 8192
memory_limiter:
check_interval: 1s
limit_mib: 4000
spike_limit_mib: 800
resource:
attributes:
- key: deployment.environment
value: production
action: upsert
attributes:
actions:
- key: http.user_agent
action: delete
tail_sampling:
decision_wait: 10s
num_traces: 100000
policies:
- name: errors-policy
type: status_code
status_code:
status_codes: [ERROR]
- name: slow-policy
type: latency
latency:
threshold_ms: 500
- name: baseline
type: probabilistic
probabilistic:
sampling_percentage: 5
exporters:
otlphttp/tempo:
endpoint: http://tempo.observability:4318
prometheusremotewrite:
endpoint: http://mimir.observability/api/v1/push
external_labels:
cluster: prod-east-1
loki:
endpoint: http://loki.observability:3100/loki/api/v1/push
debug:
verbosity: basic
service:
pipelines:
traces:
receivers: [otlp, jaeger, zipkin]
processors: [memory_limiter, resource, attributes, tail_sampling, batch]
exporters: [otlphttp/tempo]
metrics:
receivers: [otlp, prometheus]
processors: [memory_limiter, resource, batch]
exporters: [prometheusremotewrite]
logs:
receivers: [otlp]
processors: [memory_limiter, resource, batch]
exporters: [loki]OTel 的关键里程碑:
可以说,到 2023 年底,“三支柱 + 一协议 + 一 Collector”的图景基本稳定。剩下的主要工作是完善各语言 SDK、扩大自动埋点覆盖、在各家云厂商和 APM 产品里落地 OTLP 接入。
下面这张表格是从实际使用角度对比四类代表性工具:
| 维度 | Nagios | Zabbix | Prometheus | Grafana Agent (Alloy) |
|---|---|---|---|---|
| 主数据模型 | 状态(OK/WARN/CRIT/UNK)+ 可选 performance data | Item(数值、文本、日志) | 标签化时间序列 | 标签化时间序列 + OTLP |
| 标识符 | host + service | host + item key | metric + labels | metric + labels |
| 时间精度 | 秒 | 秒 | 毫秒 | 毫秒 |
| Push / Pull | Pull(主机 exec 插件) | 双向(主动/被动) | Pull 为主,Pushgateway 辅 | 双向(scrape + OTLP push) |
| 服务发现 | 手写配置 | 低级自动发现(LLD) | Kubernetes/Consul/EC2/DNS | 复用 Prometheus SD |
| 告警 | 外置(notifications.cfg) | 内置(trigger) | 内置(rules)+ Alertmanager | 通常交给 Mimir / Prometheus |
| 配置方式 | 静态 .cfg 文件 | Web UI + API + 模板 XML | YAML | HCL (Flow / Alloy Config) |
| 存储 | 状态文件 + 外部 RRD/PNP4Nagios | MySQL/PostgreSQL/TimescaleDB | 本地 TSDB;远程 Mimir/VictoriaMetrics | 无状态,转发 |
| 典型部署规模 | 百~千主机 | 千~万主机 | 万~百万 series 单实例 | Sidecar / DaemonSet |
Nagios 的基本单位是 check 结果——每次 check 返回一个状态和一行文本描述,可选附带 performance data。Performance data 是 Nagios 1.x 开始就有的扩展,格式是:
'label'=value[UOM];[warn];[crit];[min];[max]
比如:
OK - load average: 0.42, 0.35, 0.30|load1=0.420;5.0;10.0;0; load5=0.350;4.0;8.0;0; load15=0.300;3.0;6.0;0;
Nagios 本身不存时序数据,performance data 通过
process_performance_data 配置项导出给 RRDtool
或其他工具:
process_performance_data=1
service_perfdata_file=/var/lib/nagios/service-perfdata
service_perfdata_file_template=DATATYPE::SERVICEPERFDATA\tTIMET::$TIMET$\tHOSTNAME::$HOSTNAME$\tSERVICEDESC::$SERVICEDESC$\tSERVICEPERFDATA::$SERVICEPERFDATA$\tSERVICECHECKCOMMAND::$SERVICECHECKCOMMAND$\tHOSTSTATE::$HOSTSTATE$\tHOSTSTATETYPE::$HOSTSTATETYPE$\tSERVICESTATE::$SERVICESTATE$\tSERVICESTATETYPE::$SERVICESTATETYPE$
service_perfdata_file_mode=a
service_perfdata_file_processing_interval=15
service_perfdata_file_processing_command=process-service-perfdata这种数据模型在指标维度上非常弱——你想按”机房”聚合
load1?Nagios 原生不支持。
Zabbix 的 Item 有更丰富的类型:
Numeric (unsigned):整数Numeric (float):浮点Character:短字符串Log:日志Text:长文本Item 的 Key
是一个带参数的函数签名:system.cpu.util[,idle,avg1]
表示 CPU 在过去 1 分钟空闲率的均值。
Zabbix 的聚合主要靠”应用(Application)“和”主机组(Host Group)“两个维度,标签化是 4.x 版本之后才加入的,并且不像 Prometheus 那样是数据模型的第一等公民。
Prometheus 的一个时间序列由 metric name + labels 唯一标识。所有聚合、过滤、关联都围绕标签进行。Prometheus 定义了四种 metric 类型:
rate() 求速率)样例暴露格式(/metrics endpoint):
# HELP http_requests_total Total HTTP requests
# TYPE http_requests_total counter
http_requests_total{method="GET",handler="/api/users",status="200"} 12453
http_requests_total{method="POST",handler="/api/users",status="201"} 389
# HELP http_request_duration_seconds HTTP latency
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{handler="/api/users",le="0.005"} 8800
http_request_duration_seconds_bucket{handler="/api/users",le="0.01"} 10200
http_request_duration_seconds_bucket{handler="/api/users",le="0.05"} 12100
http_request_duration_seconds_bucket{handler="/api/users",le="0.1"} 12400
http_request_duration_seconds_bucket{handler="/api/users",le="+Inf"} 12453
http_request_duration_seconds_count{handler="/api/users"} 12453
http_request_duration_seconds_sum{handler="/api/users"} 381.22
# HELP node_memory_MemAvailable_bytes Memory available
# TYPE node_memory_MemAvailable_bytes gauge
node_memory_MemAvailable_bytes 8.382074880e+09
Grafana Agent(后改名 Alloy)本身不是存储,是采集和转发层。它的数据模型就是它承载的协议的数据模型:
Alloy 配置用的是 HCL 风格的流式组件:
# config.alloy
prometheus.remote_write "mimir" {
endpoint {
url = "http://mimir.observability/api/v1/push"
}
}
prometheus.scrape "node" {
targets = [
{"__address__" = "localhost:9100"},
]
forward_to = [prometheus.remote_write.mimir.receiver]
}
otelcol.receiver.otlp "default" {
grpc {}
http {}
output {
metrics = [otelcol.processor.batch.default.input]
logs = [otelcol.processor.batch.default.input]
traces = [otelcol.processor.batch.default.input]
}
}
otelcol.processor.batch "default" {
output {
metrics = [otelcol.exporter.prometheus.mimir.input]
logs = [otelcol.exporter.loki.default.input]
traces = [otelcol.exporter.otlp.tempo.input]
}
}
Prometheus 作者们对 pull 模型的偏爱有明确理由(官方 FAQ 有专门一节),核心几点:
curl http://pod:8080/metrics
就能看到当前数据。Pull 不是万能的。以下场景 Push 反而更合适:
Prometheus 为短生命周期 job 提供了
Pushgateway。应用把指标 push 到
Pushgateway,Pushgateway 暴露 /metrics 供
Prometheus 抓。示例:
# Shell Job 结束时上报
cat <<EOF | curl --data-binary @- http://pushgateway.example.com:9091/metrics/job/backup/instance/db01
# TYPE backup_last_success_unixtime gauge
backup_last_success_unixtime $(date +%s)
# TYPE backup_duration_seconds gauge
backup_duration_seconds 3742
# TYPE backup_size_bytes gauge
backup_size_bytes 84521938432
EOF但是 Pushgateway 有著名的坑,后面工程坑点一节会详细说。
StatsD 是另一种极端——纯 UDP Push,无 ACK。应用侧的 SDK 通常长这样:
import statsd
c = statsd.StatsClient('localhost', 8125, prefix='myapp')
c.incr('api.login.attempt')
with c.timer('api.login.latency'):
do_login()
c.gauge('queue.length', current_queue_size())UDP 的好处是发送 0 成本(不等响应),即使 StatsD daemon 挂了应用也不会卡。代价是数据可能丢失(内核 socket buffer 满、网络丢包),对于需要”每一个事件都不能丢”的场景不合适。
OpenTelemetry 的默认协议 OTLP 是 push-based——SDK
把遥测数据主动推送给 Collector(或直接推送给后端)。但 OTel
Collector 的 Prometheus receiver 可以反过来 scrape 现有的
/metrics endpoint,把 pull 转成内部 push
管道。
推荐组合:
/metrics endpoint,Collector
以 Prometheus receiver 形式 scrapeGrafana Agent Flow / Alloy 最大的卖点就是”同时支持 push
和 pull”——它内部可以定义一个 prometheus.scrape
组件主动拉,也可以定义 otelcol.receiver.otlp
组件接收推送,最后统一导出到后端。这让它在”混合 legacy +
新系统”的迁移场景特别好用。
这一节结合国内常见的演进路径给一个时间轴。大公司和中小公司的演进节奏并不完全同步,但大致可以分成三个阶段。
这个阶段的主要特征:
典型架构:
[Zabbix Agent × N台主机]
↓ 10051
[Zabbix Proxy × 几个机房]
↓
[Zabbix Server + MySQL]
↓
[Grafana(可选,用 Zabbix datasource)]
痛点:
这个阶段的背景是 Kubernetes、Docker、Service Mesh 的普及。老 Zabbix 对短生命周期的容器监控非常吃力,而 Prometheus 天生为容器化设计。
典型架构:
[App Pod(暴露 /metrics)]
↑ scrape
[Prometheus + kube-state-metrics + node-exporter]
↓ remote_write
[Thanos / VictoriaMetrics / Cortex]
↓
[Grafana + Alertmanager]
这个阶段的关键变化:
prometheus_client.Counter国内公开资料里,这个阶段有几个值得一提的公司实践:
随着微服务数量进一步膨胀、多语言栈(Go/Java/Python/Node/Rust/C++)并存、追踪与日志需求涌现,团队逐渐意识到单靠 Prometheus 不够。新阶段的特征:
典型架构:
[App with OTel SDK]
↓ OTLP/gRPC
[OTel Collector (DaemonSet)]
↓ OTLP/gRPC
[OTel Collector Gateway (StatefulSet)]
↓ 分流
├→ Mimir (Metrics)
├→ Tempo (Traces)
├→ Loki (Logs)
└→ Pyroscope (Profiles)
↓
[Grafana]
典型挑战:
qps.api.login,有的用
api_login_count)统一到 OTel Semantic
Conventions标签(Label / Tag)是现代监控的心脏。不同工具对它的处理差异极大。
Graphite 的命名把所有维度编码进字符串:
datacenter.us-east-1.cluster.web.host.web01.cpu.idle
datacenter.us-east-1.cluster.web.host.web01.cpu.user
datacenter.us-east-1.cluster.web.host.web02.cpu.idle
datacenter.us-west-2.cluster.web.host.web11.cpu.idle
要聚合”所有 us-east-1 的 cpu.idle 均值”:
averageSeries(datacenter.us-east-1.cluster.web.host.*.cpu.idle)
要按 datacenter 分组聚合,Graphite 需要
groupByNode:
averageSeries(groupByNode(datacenter.*.cluster.web.host.*.cpu.idle, 1, "sumSeries"))
能用,但需要知道维度在命名里的位置(第 1 节?第 3 节?),不灵活。
Prometheus 把维度放进独立的 label 空间:
cpu_idle{datacenter="us-east-1", cluster="web", host="web01"}
cpu_idle{datacenter="us-east-1", cluster="web", host="web02"}
cpu_idle{datacenter="us-west-2", cluster="web", host="web11"}
所有聚合都是一等公民:
# 按 datacenter 聚合
avg by (datacenter) (cpu_idle)
# 过滤特定条件
avg by (datacenter) (cpu_idle{cluster="web"})
# 多维度组合
avg by (datacenter, cluster) (cpu_idle)
# 排名
topk(5, cpu_idle)
# 时间窗口内最大值的按 cluster 排序
bottomk(3, max by (cluster) (cpu_idle[1h]))
这种表达力差距是根本性的,也是 Prometheus 取代 Graphite 的核心原因。
InfluxDB 把列分两类:
一次写入:
cpu,host=web01,dc=us-east-1 idle=42.3,user=12.1,system=3.4 1700000000000000000
^measurement ^tags ^fields ^timestamp ns
InfluxDB 的好处是能一次插入多个 field(CPU 的 idle/user/system),Prometheus 需要多个 metric 名字。但它的 tag 索引在高基数下同样会爆。
OpenTelemetry 不叫 label,叫 Attributes。OTel Attributes 是键值对,值可以是 string、bool、int、double、array。
当 OTel 指标导出到 Prometheus 时,attribute 映射为 label,但有规则:
. →
_,http.method →
http_method_seconds、_bytes)OTel Collector 的 prometheus exporter
配置里可以控制这些:
exporters:
prometheus:
endpoint: 0.0.0.0:8889
namespace: myapp
const_labels:
cluster: prod
send_timestamps: true
metric_expiration: 5m
resource_to_telemetry_conversion:
enabled: true下面每一条都是真实环境里会掉进去的坑。
从 Zabbix 向 Prometheus 迁移时,团队通常会有 3–12 个月的双栈并行期。两套系统对同一组主机、同一组服务都有监控,同一个事件(比如磁盘满)会同时触发两套告警。On-call 在凌晨收到两条内容几乎一样但来自不同系统的短信,非常容易误判”是不是出了两件不相关的事”。
解决办法:
Prometheus 的每个 label
组合产生一条独立时间序列。如果一个指标有 5 个 label,每个
label 平均 10 个取值,总序列数是 10^5 = 10 万。如果其中一个
label 突然变成
user_id(千万级),总序列数瞬间到 10^12
级别,Prometheus 会 OOM、rule 评估变慢到几百秒。
典型的”杀手 label”:
user_idrequest_idemailip(尤其是客户端 IP)pod_name(K8s 里 pod
名字带随机后缀,频繁重建会产生大量历史 series)url(如果 URL 里带查询参数或动态段)排查:
# Prometheus TSDB 里当前活跃 series 最多的 metric
topk(10, count by (__name__)({__name__=~".+"}))
# 某个 metric 按 label 看基数
count(count by (user_id) (http_requests_total))
防御:
/api/users/12345 归一化成
/api/users/:id 再打标签真正需要高基数维度的场景,应该用 Trace 或 Event,不要用 Metric。
Prometheus pull 的前提是能连上目标的 IP:Port。在以下场景失效:
应对:
Pushgateway 设计用于短生命周期 job,把最近一次的指标永久保存(直到下一次同 job 同 instance 的 push 覆盖它)。
问题:如果 job 不再运行了(被下线了、挂了),Pushgateway 会永远保留这次 push 的最后值。于是:
backup_last_success_unixtime
停留在很久以前的时间戳,但因为值没有变化,告警条件
time() - backup_last_success_unixtime > 86400
永远不触发(值不更新,采集到的 timestamp 是 Prometheus
scrape 的时间,not push 的时间)。解决:
push_time_seconds:Pushgateway
自动为每个 group 暴露一个 push_time_seconds
指标,告警条件用
time() - push_time_seconds{job="backup"} > 86400Zabbix 存储精度到秒,Prometheus 精度到毫秒。如果你做跨系统关联分析——比如想把 Zabbix 的主机指标和 Prometheus 的应用指标放在同一张 Grafana 面板上按时间对齐——会发现 Zabbix 数据总是”整秒”,Prometheus 数据有毫秒抖动,join 时要么丢数据要么要做时间取整。
同样的问题在 Graphite(秒)和 Prometheus 之间也存在。
即使已经规划好责任边界,告警重复仍然会以其他方式出现:
group_by
配错,一条告警多条通知Alertmanager 去重规则的配置非常重要:
route:
group_by: [alertname, cluster, service]
group_wait: 30s
group_interval: 5m
repeat_interval: 4hKubernetes SD 是 Prometheus 最常用的服务发现机制。典型的失效场景:
kubernetes_sd_configs:
- role: pod
namespaces:
names:
- production # <-- 新上的 namespace 叫 prod-east-1,不在列表里结果:新 namespace 的 Pod
完全不被发现,“静默漏监控”——Prometheus 上没有任何错误,Pod
暴露的 /metrics 仍然正常,但就是没人
scrape。
防御:
absent(up{job="kubernetes-pods", namespace="prod-east-1"})Collector 默认没有内存限制。流量突增时 Collector OOM 崩溃,所有遥测数据同时中断(Metrics、Logs、Traces 都跟着一起挂)。
防御:永远在 pipeline 最前面配
memory_limiter processor:
processors:
memory_limiter:
check_interval: 1s
limit_mib: 4000
spike_limit_mib: 800如果 OTel Trace 在 SDK 侧做了 1% 的头采样(head-based sampling),Collector 里又加了 tail sampling policy,那么最终真正到达后端的 trace 比例可能远小于预期(1% × tail policy 命中率)。
规则是:头采样和尾采样二选一。头采样适合流量大、对完整性要求低的场景;尾采样适合要捕获所有错误
trace 的场景,但 Collector 需要缓存
decision_wait 时间内的所有
span,内存开销不小。
基于上面所有分析,给出一组场景化建议。
首选组合:OpenTelemetry + Prometheus(或 Mimir/VictoriaMetrics)+ Loki(或 ClickHouse)+ Tempo(或 Jaeger)+ Grafana。
理由:
渐进式迁移:
Nagios 的核心价值是”外部可用性探测”(黑盒探测)。这部分可以用 Prometheus Blackbox Exporter 直接替代,功能重合度 > 90%:
# blackbox.yml
modules:
http_2xx:
prober: http
timeout: 5s
http:
preferred_ip_protocol: ip4
valid_status_codes: [200, 301, 302]
fail_if_ssl: false
fail_if_not_ssl: true
fail_if_body_matches_regexp:
- "(?i)error"
icmp:
prober: icmp
icmp:
preferred_ip_protocol: ip4
tcp_connect:
prober: tcp配合 Prometheus scrape:
- job_name: 'blackbox'
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- https://www.example.com
- https://api.example.com/health
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115推荐 Grafana Cloud 免费额度:10k metrics series、50 GB logs、50 GB traces,对小团队够用几年。人力成本比运维开销更重要。
如果数据出海受限,自建一套 Prometheus + Loki + Tempo + Grafana 在单台中等规格机器上也能跑起来。
自建:VictoriaMetrics(或 Mimir)+ Loki + Tempo + Grafana + OTel Collector Gateway 集群。
理由:
| 场景 | Metrics | Logs | Traces | SDK |
|---|---|---|---|---|
| 小团队,新建 | Grafana Cloud | Grafana Cloud | Grafana Cloud | OTel |
| 小团队,数据不出海 | Prometheus | Loki | Tempo | OTel |
| 大团队,新建 | Mimir / VictoriaMetrics | Loki / ClickHouse | Tempo / Jaeger | OTel |
| 存量 Zabbix,渐进迁移 | Zabbix + Prometheus 并行 | Loki 新建 | Tempo / Jaeger 新建 | OTel |
| 存量 CAT / Skywalking | 保留 APM | 保留 + 新 Loki | 保留 APM,新 OTel 逐步替换 | OTel |
| SaaS 监控客户内网 | Pushgateway / OTLP push | Loki Agent push | OTLP push | OTel |
| Serverless / FaaS | OTLP push | OTLP push | OTLP push | OTel |
从 1999 年到 2024 年,监控这件事大致走过了五个时代:Nagios/Zabbix 的主机存活时代、Graphite/StatsD 的时序指标时代、Prometheus 的标签化拉取时代、OpenTracing/Jaeger 的分布式追踪时代、OpenTelemetry 的三支柱统一时代。
每一代都不是前一代的否定,而是对前一代局限的工程回应。Nagios 回答了”系统还活着吗”;Graphite 回答了”指标怎么变的”;Prometheus 回答了”任意维度切分的指标怎么变的”;Jaeger 回答了”一次请求在系统内走了哪些节点”;OpenTelemetry 试图回答”怎么把上面所有的东西用同一套数据模型和 SDK 串起来”。
再往下一代大概率会是可观测性即数据平台——所有 telemetry 数据进入统一列存(ClickHouse、Apache Iceberg 之类),Metrics / Logs / Traces / Events / Profiles 在同一份数据上可查,通过查询语言(而不是预聚合)动态生成所有仪表盘和告警。这种架构已经在几家大公司内部试点,Grafana Loki/Tempo 和 Honeycomb 都在往这个方向走。
但是”更好的工具”永远不等于”更好的可观测性”。工具只能提供能力,真正决定可观测性水平的是:团队有没有把”每一个可疑事件都记录足够多上下文维度”这件事做成肌肉记忆。回到本文开头那句话——监控只能回答你已经想到的问题,可观测性的门票是承认你想不完所有问题。
上一篇:可观测性全景:Metrics、Logs、Traces、Profiles、Events 五大支柱
下一篇:指标体系设计:USE、RED、Golden Signals 与业务 KPI
把当前热点继续串成多页阅读,而不是停在单篇消费。
2026-04-22 · architecture / observability
从控制论到云原生:拆解可观测性的五大信号支柱,对比监控与可观测性的本质区别,梳理开源/商业/SaaS 分类,以及国内互联网公司三大支柱落地现状与典型工程坑点。
2026-04-22 · architecture / observability
从 Metrics、Logs、Traces 到 Profiling、eBPF、OpenTelemetry 与 SLO 治理,面向中国工程团队的可观测性系统化手册。
2026-04-22 · architecture / observability
从 Prometheus 架构与数据模型出发,系统梳理 Remote Write、PromQL 进阶、Thanos 全局聚合、Mimir 多租户、VictoriaMetrics 性能、M3DB 原理,以及五者在大规模生产场景下的对比矩阵与迁移实践。
2026-04-22 · architecture / observability
USE 方法论适用于资源,RED 方法论适用于请求,Golden Signals 适用于服务——三套方法论各有其适用对象。本文从 Brendan Gregg、Tom Wilkie、Google SRE 的原始定义出发,构建覆盖资源→服务→业务的完整指标体系,并给出 Prometheus 命名规范、基数治理策略与可抄的指标清单。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。