惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The GitHub Blog
The GitHub Blog
S
SegmentFault 最新的问题
L
LangChain Blog
Simon Willison's Weblog
Simon Willison's Weblog
N
News and Events Feed by Topic
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
WordPress大学
WordPress大学
M
MIT News - Artificial intelligence
A
About on SuperTechFans
Microsoft Security Blog
Microsoft Security Blog
I
Intezer
Know Your Adversary
Know Your Adversary
H
Heimdal Security Blog
博客园 - 叶小钗
B
Blog RSS Feed
F
Fortinet All Blogs
Hacker News: Ask HN
Hacker News: Ask HN
A
Arctic Wolf
小众软件
小众软件
Help Net Security
Help Net Security
MongoDB | Blog
MongoDB | Blog
aimingoo的专栏
aimingoo的专栏
G
Google Developers Blog
Forbes - Security
Forbes - Security
Latest news
Latest news
AI
AI
I
InfoQ
H
Hackread – Cybersecurity News, Data Breaches, AI and More
C
CXSECURITY Database RSS Feed - CXSecurity.com
W
WeLiveSecurity
C
Cybersecurity and Infrastructure Security Agency CISA
人人都是产品经理
人人都是产品经理
Cyberwarzone
Cyberwarzone
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
J
Java Code Geeks
Engineering at Meta
Engineering at Meta
C
Cyber Attacks, Cyber Crime and Cyber Security
O
OpenAI News
博客园 - 【当耐特】
T
Threat Research - Cisco Blogs
GbyAI
GbyAI
U
Unit 42
D
Darknet – Hacking Tools, Hacker News & Cyber Security
G
GRAHAM CLULEY
Apple Machine Learning Research
Apple Machine Learning Research
宝玉的分享
宝玉的分享
Google DeepMind News
Google DeepMind News
T
Threatpost
T
The Blog of Author Tim Ferriss
罗磊的独立博客

土法炼钢兴趣小组的算法知识备份

国密算法与国密 TLS 系列索引 【系统架构设计】架构质量属性:不只是"高可用高性能" 【系统架构设计百科】告警策略:如何避免"狼来了" 【系统架构设计】CQRS:读写分离的架构哲学 【系统架构设计】空间架构:极端扩展场景的解法 【系统架构设计】微服务架构深度审视:优势、代价与适用边界 【系统架构设计】扩展性原理:水平、垂直与对角扩展 【系统架构设计】无状态设计:扩展的第一步也是最难的一步 【系统架构设计】缓存架构:从本地到分布式的多级缓存体系 【系统架构设计】管道与过滤器:Unix 哲学的架构表达 【系统架构设计】复杂性管理:架构的核心战场 【系统架构设计】消息队列架构:异步解耦的设计与陷阱 【系统架构设计】CDN 架构:全球加速的设计原理 【系统架构设计】连接池设计:被忽视的性能杀手 【系统架构设计】弹性设计模式:熔断器、舱壁与超时 【系统架构设计】高可用设计模式:冗余、故障转移与仲裁 【系统架构设计】容量规划:从拍脑袋到数据驱动 【系统架构设计】数据库扩展:分库分表的工程实践与替代方案 【系统架构设计】SLO 工程:可靠性的量化管理 【系统架构设计】性能建模:用数学思维分析系统瓶颈 【系统架构设计】混沌工程:主动验证系统的韧性 【系统架构设计】零拷贝与内存映射:数据搬运的极致优化 【系统架构设计】线程模型:从 thread-per-request 到协程 【系统架构设计】容灾架构:多活与灾备设计 【系统架构设计】数据库性能模式:索引、查询与连接管理 【系统架构设计】数据建模:从关系范式到文档模型的真实权衡 【系统架构设计】吞吐量优化:批处理、流水线与并发模型 【系统架构设计】流处理架构:从批处理到实时的范式迁移 【系统架构设计】搜索引擎架构:倒排索引之上的系统设计 【系统架构设计】时序数据架构:监控与 IoT 的存储设计 【系统架构设计】数据迁移与版本化:在线不停机的数据演进 【系统架构设计】数据湖与数据仓库:分析架构的演进路线 【系统架构设计】API 网关设计:入口层的职责边界 【系统架构设计】应用层数据一致性模式:在正确性与性能之间走钢丝 【系统架构设计】多模数据库选型:Polyglot Persistence 的工程实践 【系统架构设计】服务发现与注册:动态拓扑的基础设施 【系统架构设计】配置管理架构:从配置文件到配置中心 【系统架构设计】全链路压测:大规模系统的性能验证 【系统架构设计】幂等性设计:分布式环境下的安全重试 【系统架构设计】契约测试与 Schema 演进:服务间的信任协议 【系统架构设计】长连接与推送架构:WebSocket、SSE 与 MQTT 【系统架构设计】延迟分析:从 P50 到 P999 的全链路追踪 【系统架构设计百科】DDD 战术模式:聚合、实体与值对象 【系统架构设计百科】防腐层与开放主机服务:系统集成的 DDD 方案 【系统架构设计百科】领域事件与事件风暴:从业务到架构的桥梁 【系统架构设计百科】CQRS + Event Sourcing 完整实战:从领域建模到部署 【系统架构设计百科】DDD 与微服务:用领域模型划分服务边界 【系统架构设计】DDD 战略设计:限界上下文与上下文映射 【系统架构设计百科】认证架构:从 Session 到 JWT 到 OIDC 【系统架构设计】API 设计哲学:REST vs GraphQL vs gRPC 的真实权衡 排序算法专题:从 TimSort 到并行排序 【密码学百科】国密算法体系:SM2/SM3/SM4/SM9 全景解读 【密码学百科】承诺方案:Pedersen 承诺、向量承诺与多项式承诺 【密码学百科】不经意传输与隐私信息检索:OT、OT 扩展与 PIR 【密码学百科】门限密码学:门限签名、门限解密与分布式密钥生成 完美哈希:从理论到 gperf 实践 【密码学百科】安全多方计算:从 Yao 的混淆电路到实用 MPC 【密码学百科】同态加密:从 Paillier 到全同态加密(FHE) 【密码学百科】零知识证明系统:zk-SNARKs、zk-STARKs 与 Bulletproofs 【密码学百科】概率论与密码分析:生日攻击、差分分析与线性分析 【密码学百科】计算复杂性与归约:密码安全性证明的基石 【密码学百科】秘密共享:Shamir 方案、VSS 与安全多方计算入口 【密码学百科】椭圆曲线代数:Weierstrass 方程、点群运算与曲线选择 【密码学百科】离散对数与配对密码学:从 DLP 到 BLS 签名 【密码学百科】格密码数学基础:SVP、LWE 与格基约化 【密码学百科】抽象代数:群、环、域的密码学视角 【密码学百科】有限域算术:GF(2^n) 运算与在 AES/ECC 中的应用 【密码学百科】数论进阶:二次剩余、椭圆曲线上的 Weil 配对 【密码学百科】密码学简史:从凯撒密码到量子时代 【密码学百科】威胁模型与安全目标:CIA 三要素之外 【密码学百科】Kerckhoffs 原则与现代密码设计哲学 【密码学百科】随机性:密码学的基石 【密码学百科】信息论入门:熵、完美保密与 Shannon 定理 【密码学百科】分组密码原理:Feistel 网络与 SPN 结构 【密码学百科】AES 逐步拆解:SubBytes 到 MixColumns 的数学 【密码学百科】分组密码工作模式全览:ECB/CBC/CTR/OFB/CFB 【密码学百科】流密码:RC4 的兴衰与 ChaCha20 的崛起 【密码学百科】密码学哈希函数:MD5→SHA-2→SHA-3 的进化之路 【密码学百科】MAC 与 HMAC:消息认证的正确姿势 【密码学百科】认证加密(AEAD):GCM、ChaCha20-Poly1305 与 OCB 【密码学百科】密钥派生函数:HKDF、PBKDF2、Argon2 与密码存储 【密码学百科】公钥密码的数论基础:模运算、群、原根 【密码学百科】RSA 从原理到攻击:教科书 RSA 为什么不安全 【密码学百科】Diffie-Hellman 密钥交换与离散对数问题 【密码学百科】椭圆曲线密码学(ECC):从几何直觉到点群运算 【密码学百科】数字签名:ECDSA、EdDSA 与 Schnorr 签名 【密码学百科】现代密钥交换:X25519、ECDHE 与前向保密 【密码学百科】混合加密与 KEM/DEM 范式:ECIES 与 HPKE 【密码学百科】填充方案:PKCS#1 v1.5、OAEP 与 PSS 【密码学百科】TLS 协议全解析:从握手到 0-RTT 【密码学百科】PKI 与数字证书:信任链的构建与崩塌 【密码学百科】密码认证协议:从 SRP 到 OPAQUE 【密码学百科】零知识证明入门:如何证明你知道而不泄露 【密码学百科】安全信道构造:Noise 协议框架与 Signal 协议 【密码学百科】密钥管理工程:HSM、KMS 与密钥生命周期 【密码学百科】侧信道攻击:从时序攻击到功耗分析 【密码学百科】密码学实现陷阱:三层漏洞分类、审计工具链与系统性预防 密码敏捷性:如何设计可升级的密码系统 【密码学百科】OpenSSL/BoringSSL 架构剖析:ENGINE、Provider 与 FIPS 模块 排序基准测试:用数据说话
【可观测性工程】可观测性 vs 监控:从 Zabbix/Nagios 到 OpenTelemetry 的二十年
2026-04-22 · via 土法炼钢兴趣小组的算法知识备份
时间线

在很多团队的工程语境里,“监控”和”可观测性”被当作同义词使用:有同事把 Grafana 仪表盘叫做”监控大盘”,把 Prometheus 告警叫做”监控告警”,把 Jaeger 里的 Trace 视图也叫做”监控链路”。这种用法在日常沟通中无伤大雅,但它掩盖了一个在架构层面非常关键的事实——监控(Monitoring)和可观测性(Observability)不是一个东西的两个名字,它们来自完全不同的认知模型,对应完全不同的数据组织方式,解决完全不同的问题。

把这两个概念混用的代价,会在系统规模上去、故障类型变得”离奇”之后显现出来。一个典型的症状是:监控面板上所有指标全部绿色,但用户已经在投诉;告警没有响,但 SLO 已经破了;值班同事盯着十几个仪表盘逐个切换,却找不到故障根因。这时候人们才会意识到,自己搭的其实是一套”监控系统”,而不是一套”可观测性系统”。

本文尝试把这段历史捋清楚。从 1999 年 Nagios 1.0 发布,到 2019 年 OpenTelemetry 合并成立,再到 2023 年 OpenTelemetry Metrics 与 Logs 正式 GA,整整二十四年时间里,行业在这件事上反复试错了至少五轮。每一轮的设计选择——推(push)还是拉(pull)、层级命名还是标签化、文本协议还是 gRPC、单一数据模型还是三支柱统一——都有非常具体的历史原因,也都留下了非常具体的工程坑点。理解这段历史,比盲目追新工具更重要。

一、监控的定义与局限

1.1 监控的核心范式

监控的核心范式可以用一个最小环路来概括:预设问题 → 收集指标 → 阈值触发 → 人工响应。这个环路的起点是”预设问题”,也就是说,在系统上线之前,运维工程师必须已经能够列出所有他关心的指标,才能针对每个指标配置采集脚本和告警阈值。

举个最经典的例子:一个 Web 服务器,运维会预设如下几个问题:

  • 进程是否还活着?(用 pidof 或 TCP 探测回答)
  • 磁盘还剩多少空间?(用 df -h 回答)
  • CPU 负载是否过高?(用 uptime/proc/loadavg 回答)
  • HTTP 是否能返回 200?(用 curl 回答)

每一个问题都对应一个脚本,每一个脚本的输出都对应一个数值,每一个数值都对应一个阈值。只要系统在预设范围内运行,监控就能工作得很好。

1.2 Nagios 插件模型

这套范式最成熟的实现是 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/loadavg
  • check_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:

这份配置本质上是在做一件事:把每一个”我关心的问题”翻译成一次周期性的命令执行和一次阈值比较

1.3 两类告警:存活检测与阈值告警

传统监控的告警基本可以归为两大类:

存活检测(Liveness Check):只关心”在不在”。端口能不能连上?进程是不是还存在?HTTP 能不能返回 2xx?这类检查的输出是布尔型的,只有 UP / DOWN 两种状态。它处理的是最基础的可用性问题——当一台机器宕机、一个进程崩溃、一个网络分区形成时,存活检测能在分钟级别发现并通知。

阈值告警(Threshold Alert):关心”量”。CPU 超过 80% 告警、磁盘剩余少于 10% 告警、每分钟请求量少于 100 告警。阈值告警的本质是把一个连续变量(指标值)离散化成几个状态(OK / WARNING / CRITICAL)。

两类告警合起来能覆盖绝大多数”系统层”问题。在单体应用时代、物理机或小规模虚拟机时代,这套机制工作得相当好。

1.4 已知未知 vs 未知未知

但是这种范式有一个根本性的限制。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.”

翻译成中文大致是:

  • 已知的已知(Known Knowns):我知道,而且我知道它是什么
  • 已知的未知(Known Unknowns):我知道有这个问题,但我不知道当前的取值
  • 未知的未知(Unknown Unknowns):我根本没想到还会有这种问题

传统监控能覆盖的范围是”已知的未知”——我知道 CPU 会影响性能(这是”已知”),所以我监控 CPU 使用率(用一个指标填充”未知”的取值)。当我想监控 CPU 时,我的认知里必须已经存在”CPU 使用率会影响系统行为”这个假设。

问题在于:在真实的生产故障里,“未知的未知”占了绝对大多数。比如:

  • 某个下游第三方服务在凌晨两点对特定 region 的请求开始返回 5xx,影响了 0.3% 的用户登录
  • 某段代码在商品 ID 以 0 开头时走了错误的分支,只对占总量 1.1% 的商品生效
  • 某个数据库连接池在请求并发达到 137 时开始排队,超过 147 时开始抛异常,但平均并发只有 80
  • 某个 JSON 解析库在处理超过 64 KB 的数组时有指数级性能退化,只影响极少数查询

这些故障的共同特征是:事前没有人会为它们配置监控,因为没有人会想到要配置。监控系统天然只能回答”你已经想到的问题”——这不是工具的 bug,是架构层面的约束。

1.5 监控的认知上限

换句话说,监控的认知上限等于设计者在系统上线前能想到的问题集合。这个集合是有限的、静态的,而生产系统的故障空间是无限的、动态的。当系统复杂到一定程度(微服务、分布式、多租户、异构),你事前能枚举的问题和实际会发生的问题之间的差距会呈指数级拉大。

这就是为什么在 2010 年代后期,随着微服务架构普及,传统监控开始变得越来越力不从心。工程师发现自己配了成百上千条告警规则,但一出事还是只能靠猜。

二、可观测性的定义

2.1 来自控制论的原始定义

“可观测性”这个词并不是云原生时代发明的。它来自 Rudolf Kálmán(卡尔曼滤波器的那个 Kálmán)1960 年在控制论中给出的严格定义:

一个系统是可观测的(observable),当且仅当可以通过其输出(output)的有限次观测推断出其内部状态(internal state)。

在控制论语境下,这是一个线性代数问题——看观测矩阵的秩。拿到软件工程领域后,Charity Majors 给出了工程化的翻译:

可观测性是指你能够回答关于系统内部状态的任意问题,而无需事先知道你要问什么问题,也无需发布新代码来收集额外的遥测数据。

这个定义里有两个关键限定:

  1. 任意问题:不是一组预设问题,而是任意问题
  2. 无需发布新代码:意味着所需信息必须已经被事先收集

2.2 从”更多监控”到”任意问题可回答”

很多团队把”可观测性”理解成”更多的监控”——上更多的仪表盘、配更多的告警规则、接入更多的指标。这种理解是错的。

可观测性的核心不是”量”,是”维度”。如果你采集了一百个指标,但每个指标只有”服务名”一个标签,那么无论这一百个指标的数量有多大,你能回答的问题仍然是静态的一组——你永远无法用这些数据回答”某个特定用户的请求为什么变慢了”。

可观测性需要的是:在系统的每一个关键事件上,记录足够多的上下文维度。当你记录的维度足够多时,事后可以用任意维度的组合去切分和聚合数据。这才是”任意问题可回答”的真正含义。

2.3 高基数和高维度

这就引出了两个工程上的关键概念:

高基数(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)方案。

2.4 为什么 p99 延迟比 avg 延迟更能发现问题

再举一个例子说明维度的重要性:

假设一个服务总共 10000 QPS,平均延迟 50 ms。看起来很健康。但如果你把数据按 user_id 切开,发现其中 10 QPS(万分之一)的平均延迟是 5000 ms,这 10 QPS 全部来自某个大客户,而这个大客户刚刚签了八位数的合同——你的”平均延迟 50 ms”实际上已经是一个严重事故。

如果你只记录”平均延迟”这一个标量,永远发现不了这种问题。如果你记录了分布(p50/p95/p99/p999),能发现”有人慢”;如果你记录了分布 + user_id 维度,才能发现”是谁慢”。

所以可观测性的工程落地,本质上是在回答一个问题:如何在可承受的存储和查询成本内,保留足够多的事件维度。这是整个领域过去十年的主线,也是 OpenTelemetry 设计的主要驱动力。

三、二十年演进时间线

下面按时间段梳理整个演进过程。每一段都有它自己的时代背景,也都有解决和未解决的问题。

3.1 1999–2006:Nagios 时代

1999 年,Ethan Galstad 在自己的车库里写出了 NetSaint,后来因为商标原因改名为 Nagios(“Nagios Ain’t Gonna Insist On Sainthood”,一个递归缩写)。这是服务器监控工具的事实标准的起点。

当时的时代背景是:

  • 服务器基本都是物理机,数量从几十台到几千台
  • 操作系统以 Linux 和各种 Unix(Solaris、AIX、HP-UX)为主
  • 部署以”一台机器一个服务”为主,服务之间通过 IP 和端口直连
  • 故障模式以硬件故障、进程崩溃、磁盘满、网络不通为主

Nagios 的设计完美契合这个时代:

  • 基于主机(host)和服务(service)两级的资源模型
  • 插件可以用任何语言写,只要遵守退出码约定
  • 配置文件纯文本,适合放进版本控制系统
  • 告警通过邮件、短信、自定义脚本发送

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):

  • 被动模式:Server 主动连 Agent 端口(默认 10050)拉取数据
  • 主动模式:Agent 主动连 Server 端口(默认 10051)推送数据

一段典型的 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()}&gt;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),超过这个数字通常要做分区或者分成多套。

这个时代的局限现在看得很清楚:

  • 数据模型是主机中心,不适合容器这种短生命周期资源
  • 阈值告警是唯一手段,缺少对分布、趋势、相关性的建模
  • 配置管理是重活,尤其是 Nagios 的文本配置,上千主机的集群配置维护起来极其痛苦
  • 可视化薄弱,Zabbix 自带图表勉强能看,Nagios 完全依赖第三方

3.2 2006–2012:时序指标时代

2006 年,Chris Davis 在 Orbitz 写了 Graphite,后来捐献给开源社区。Graphite 是第一个被广泛使用的”纯时序指标”系统。它的核心组件是:

  • Carbon:接收 metrics 的 daemon,协议极其简单——文本 path value timestamp\n
  • Whisper:基于文件的固定大小环形缓冲时序存储(类似 RRDtool)
  • Graphite-Web:基于 Django 的查询和图形化界面

Graphite 的指标命名是层级(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 2003

2011 年,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 挂了应用也不会受影响。

这个时代还有几个值得一提的工具:

  • Ganglia:加州大学伯克利分校开发,主要用于 HPC 集群,UDP multicast 上报,XML 协议
  • collectd:C 写的轻量 agent,插件化,是 Prometheus node_exporter 之前最流行的主机指标采集器
  • OpenTSDB:2010 年 StumbleUpon 开源,基于 HBase 的大规模时序存储
  • InfluxDB:2013 年开源,自研存储引擎 TSM-tree,Go 语言写的

这个时代已经跟 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 就很难表达。

3.3 2012–2016:Prometheus 革命

Prometheus 的诞生直接针对上述问题。2012 年,Matt T. Proud 和 Julius Volz 在 SoundCloud(德国柏林的一家音乐流媒体公司)内部受 Google Borgmon 启发开始写 Prometheus。SoundCloud 当时面临的问题是:他们在从单体向微服务迁移,现有的 Graphite + StatsD 架构无法应付服务实例数量的爆炸。

Prometheus 的几个核心设计选择:

  1. 标签系统(Labels)替代层级命名。每个时间序列由 metric name + labels 唯一标识:
http_requests_total{method="GET", handler="/api/v1/users", status="200", instance="10.0.0.1:8080"}
  1. 拉取模型(Pull-based)。Prometheus Server 主动从目标抓取(scrape),而不是让目标推送过来。

  2. 服务发现(Service Discovery)。Prometheus 原生支持 Consul、Kubernetes、EC2、DNS SRV 等服务发现机制,目标列表动态变化。

  3. 自研查询语言 PromQL。表达聚合、过滤、函数运算非常自然。

  4. 本地 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: pod

Prometheus 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: ticket

Alertmanager 负责告警的去重、分组、路由、静默:

# 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”。

3.4 2016–2019:分布式追踪标准化

指标解决了”聚合状态”的问题,但对跨服务的单次请求链路追溯无能为力。这个空缺由分布式追踪(Distributed Tracing)填补。

历史上分布式追踪的源头是 Google 2010 年发表的 Dapper 论文。Twitter 受此启发开源了 Zipkin(2012);Uber 基于 Zipkin 的思想做了更大规模的 Jaeger(2017)。

但是每家公司的 SDK 都是自己的,埋点代码没法在不同系统间迁移,这促成了 OpenTracing 项目的诞生。OpenTracing 由 Ben Sigelman(Dapper 作者之一)于 2016 年发起,定义了一套语言无关的 API 规范:

  • Tracer:创建 Span 的工厂
  • Span:一次操作的时间区间,带标签和日志
  • SpanContext:跨进程传播的上下文
  • Carrier:承载 SpanContext 的介质(HTTP header、Kafka header 等)

典型代码(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 result

OpenTracing 只是规范,不是实现。真正的 Tracer 由 Jaeger、Zipkin 等项目各自提供兼容的实现。

2018 年,Google 开源了 OpenCensus,定位类似——但 OpenCensus 野心更大,它同时覆盖了 Metrics 和 Traces,而且是一个”库”而不仅是”规范”,开箱即用。OpenCensus 来自 Google 内部的 Census 库。

于是行业里出现了”双雄分裂”的局面:

维度 OpenTracing OpenCensus
发起方 LightStep 等 Google
覆盖范围 仅 Traces Traces + Metrics
定位 API 规范 API + SDK
加入 CNCF 2016 2019(孵化)

两个项目的用户基数都不小,但谁也吃不掉对方,中间件作者要做选择题:到底实现哪个接口?用户也头疼:我用了 A,换库的时候发现中间件只支持 B。

3.5 2019 至今:OpenTelemetry 统一

2019 年 5 月,在 KubeCon Barcelona 上,两个项目的 Steering Committee 共同宣布合并为 OpenTelemetry(OTel)。OpenTelemetry 成为 CNCF 项目(现为活跃程度仅次于 Kubernetes 的第二大项目)。

OpenTelemetry 的组成:

  1. API 规范:定义各语言的 API 接口
  2. SDK 实现:提供默认实现(Go、Java、Python、JS、C++、.NET、Rust、PHP、Ruby、Swift、Erlang 等)
  3. OTLP(OpenTelemetry Protocol):基于 gRPC / HTTP + Protobuf 的统一传输协议
  4. OpenTelemetry Collector:vendor-neutral 的数据管道,接收、处理、导出遥测数据
  5. Semantic Conventions:统一语义约定(比如 HTTP 属性、DB 属性、RPC 属性的标准命名)
  6. Instrumentation libraries:开箱即用的自动埋点库(比如 Java Agent、Python opentelemetry-instrument)

一份 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 的关键里程碑:

  • 2019-05:OpenTracing + OpenCensus 合并
  • 2021-02:Traces 规范 GA
  • 2022-03:OTLP GA
  • 2023-11:Metrics 规范 GA,Logs 规范 GA
  • 2024+:Profiles(持续剖析,continuous profiling)进入孵化

可以说,到 2023 年底,“三支柱 + 一协议 + 一 Collector”的图景基本稳定。剩下的主要工作是完善各语言 SDK、扩大自动埋点覆盖、在各家云厂商和 APM 产品里落地 OTLP 接入。

四、Zabbix / Nagios / Prometheus / Grafana Agent 数据模型差异

下面这张表格是从实际使用角度对比四类代表性工具:

维度 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

4.1 Nagios 数据模型

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 原生不支持。

4.2 Zabbix 数据模型

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 那样是数据模型的第一等公民。

4.3 Prometheus 数据模型

Prometheus 的一个时间序列由 metric name + labels 唯一标识。所有聚合、过滤、关联都围绕标签进行。Prometheus 定义了四种 metric 类型:

  • Counter:只增不减的累积值(需要用 rate() 求速率)
  • Gauge:可增可减的瞬时值
  • Histogram:预定义 bucket 的分布
  • Summary:客户端计算的分位数(不推荐在服务端聚合场景使用)

样例暴露格式(/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

4.4 Grafana Agent / Alloy 数据模型

Grafana Agent(后改名 Alloy)本身不是存储,是采集和转发层。它的数据模型就是它承载的协议的数据模型:

  • 采集 Prometheus → 标签化指标
  • 接收 OTLP → OTel 数据模型
  • 采集日志 → Loki 的标签 + body 模型
  • 采集 Trace → OTLP/Jaeger/Zipkin 格式

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]
  }
}

五、推送(Push)vs 拉取(Pull)模型

5.1 Pull 的优势——Prometheus 的选择

Prometheus 作者们对 pull 模型的偏爱有明确理由(官方 FAQ 有专门一节),核心几点:

  1. 天然的健康检查:scrape 失败本身就是一个信号。如果 pull 不到,你立刻知道目标掉线了。Push 模式下,应用挂了,监控侧可能只会看到”没数据”,不知道是挂了还是没流量。
  2. 统一控制采集频率:采集间隔由 Prometheus 决定,不同环境可以不同。Push 模式里如果客户端频率乱改,服务端只能接着。
  3. 服务发现与自动目标列表:Prometheus 通过 SD 自动发现目标,目标变动不用动配置。
  4. 客户端简单:应用只需要暴露一个 HTTP endpoint,不需要维护连接。
  5. 易于调试curl http://pod:8080/metrics 就能看到当前数据。

5.2 Push 的场景

Pull 不是万能的。以下场景 Push 反而更合适:

  • 短生命周期 job:一个 cron job 跑 10 秒就退出,Prometheus 根本来不及 scrape。
  • 防火墙穿透:云上监控本地,或者监控在公网访问不到的网络内部。
  • 事件型数据:业务埋点(注册、下单),用计数器 + pull 可以,但某些场景天然是事件流。
  • Serverless/FaaS:函数生命周期以秒计,无法维持 scrape endpoint。

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 有著名的坑,后面工程坑点一节会详细说。

5.3 StatsD 的 UDP Push:极简但无确认

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 满、网络丢包),对于需要”每一个事件都不能丢”的场景不合适。

5.4 OTel 的立场:OTLP 是 Push,Collector 可以 Scrape

OpenTelemetry 的默认协议 OTLP 是 push-based——SDK 把遥测数据主动推送给 Collector(或直接推送给后端)。但 OTel Collector 的 Prometheus receiver 可以反过来 scrape 现有的 /metrics endpoint,把 pull 转成内部 push 管道。

推荐组合:

  • 应用层:SDK 通过 OTLP push 到 Collector Agent(sidecar 或 DaemonSet)
  • 老系统:保留 /metrics endpoint,Collector 以 Prometheus receiver 形式 scrape
  • 出口:Collector 把数据批量 push 到 Mimir / Prometheus remote_write / Tempo / Loki

5.5 Grafana Agent Flow:两者兼得

Grafana Agent Flow / Alloy 最大的卖点就是”同时支持 push 和 pull”——它内部可以定义一个 prometheus.scrape 组件主动拉,也可以定义 otelcol.receiver.otlp 组件接收推送,最后统一导出到后端。这让它在”混合 legacy + 新系统”的迁移场景特别好用。

六、国内从 Zabbix 到 Prometheus 再到 OTel 的典型迁移路径

这一节结合国内常见的演进路径给一个时间轴。大公司和中小公司的演进节奏并不完全同步,但大致可以分成三个阶段。

6.1 阶段一:Zabbix 时代(约 2010–2015)

这个阶段的主要特征:

  • 运维驱动:监控是 OP 部门的事,开发基本不碰
  • 主机中心:监控对象主要是物理机、虚拟机
  • 阈值告警:大部分告警是基于固定阈值
  • IM + 短信:告警通过短信和企业 IM 推送

典型架构:

[Zabbix Agent × N台主机]
         ↓ 10051
[Zabbix Proxy × 几个机房]
         ↓
[Zabbix Server + MySQL]
         ↓
[Grafana(可选,用 Zabbix datasource)]

痛点:

  • MySQL 慢——Zabbix 的 history 表经常成为瓶颈,很多公司要做分区或切 TimescaleDB
  • 容器化之后 Zabbix 的主机模型不适配
  • 应用层指标(QPS、RT、错误率)上报困难,需要大量 UserParameter 定制

6.2 阶段二:Prometheus + Grafana 时代(约 2015–2020)

这个阶段的背景是 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
  • 标签化:metric 带上 service、version、region、pod 等多维标签
  • PromQL 成为技能点:写 SRE 招聘 JD 必加 PromQL
  • 双栈并行:Zabbix 继续做主机监控,Prometheus 做应用监控

国内公开资料里,这个阶段有几个值得一提的公司实践:

  • 美团:最早大规模自研监控平台的公司之一,内部有 CAT(Central Application Tracking,点评系统,2012 年开源)做 APM,Falcon(2015 年开源)做指标监控。美团技术博客《统一监控平台的架构演进》(2019)详细描述了从 Falcon + CAT 到新统一平台的过程。
  • 腾讯:Tars 框架自带监控,公开博客《Prometheus 在腾讯云的落地实践》(2020)介绍了它们在 TKE 上 Prometheus 改造的实践。
  • 滴滴:Nightingale(夜莺)开源项目,国内社区影响力很大,兼容 Prometheus 生态并做了本土化的告警管理、订阅、UI 本地化等改进。
  • 阿里:内部有 EagleEye(鹰眼)做 Tracing,Sunfire 做监控,公开文章介绍过它们与 Prometheus 的融合。
  • 字节跳动:Argos、ByteMonitor 等内部系统,基于 ClickHouse 或自研 TSDB。

6.3 阶段三:OTel + 自建平台时代(2020 至今)

随着微服务数量进一步膨胀、多语言栈(Go/Java/Python/Node/Rust/C++)并存、追踪与日志需求涌现,团队逐渐意识到单靠 Prometheus 不够。新阶段的特征:

  • 统一埋点:用 OTel SDK 替换各语言各自的 Prometheus client、Jaeger client、logger
  • 三支柱合流:Metrics(Prometheus / Mimir / VictoriaMetrics)+ Traces(Tempo / Jaeger)+ Logs(Loki / ClickHouse / ES)
  • Collector 中台:所有遥测数据先汇到 Collector 集群,再分流到后端
  • 持续剖析(Continuous Profiling):Pyroscope / Parca 接入
  • SLO 驱动告警:用 error budget 替代纯阈值

典型架构:

[App with OTel SDK]
      ↓ OTLP/gRPC
[OTel Collector (DaemonSet)]
      ↓ OTLP/gRPC
[OTel Collector Gateway (StatefulSet)]
      ↓ 分流
  ├→ Mimir (Metrics)
  ├→ Tempo (Traces)
  ├→ Loki (Logs)
  └→ Pyroscope (Profiles)
           ↓
         [Grafana]

6.4 迁移挑战

典型挑战:

  1. 历史告警规则迁移:Zabbix trigger 语法和 PromQL 完全不同,需要逐条重写
  2. 双栈并行维护:Zabbix 和 Prometheus 同时告警,需要严格分工(哪些指标归谁)
  3. 数据对齐:Zabbix 秒级 vs Prometheus 毫秒级,跨系统做 join 困难
  4. 埋点规范:从不统一的 metric 命名(有的用 qps.api.login,有的用 api_login_count)统一到 OTel Semantic Conventions
  5. 人的迁移:OP 团队的 Zabbix 技能栈如何过渡到开发为主的 Prometheus/OTel 技能栈

七、Tag / Label 对比

标签(Label / Tag)是现代监控的心脏。不同工具对它的处理差异极大。

7.1 Graphite 层级命名

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 节?),不灵活。

7.2 Prometheus Label

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 的核心原因。

7.3 InfluxDB 的 Tag vs Field

InfluxDB 把列分两类:

  • Tag:被索引的维度(类似 Prometheus label),高基数成本高
  • Field:不被索引的数值列

一次写入:

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 索引在高基数下同样会爆。

7.4 OTel Attributes 与 Prometheus Labels 的映射

OpenTelemetry 不叫 label,叫 Attributes。OTel Attributes 是键值对,值可以是 string、bool、int、double、array。

当 OTel 指标导出到 Prometheus 时,attribute 映射为 label,但有规则:

  • 非法字符替换:._http.methodhttp_method
  • 资源属性(service.name、deployment.environment)可选合并进 label 或作为 external_label
  • 单位后缀:OTel 规定 metric 名字不带单位,Prometheus 习惯带(_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

八、工程坑点

下面每一条都是真实环境里会掉进去的坑。

8.1 双栈并行期:告警重复

从 Zabbix 向 Prometheus 迁移时,团队通常会有 3–12 个月的双栈并行期。两套系统对同一组主机、同一组服务都有监控,同一个事件(比如磁盘满)会同时触发两套告警。On-call 在凌晨收到两条内容几乎一样但来自不同系统的短信,非常容易误判”是不是出了两件不相关的事”。

解决办法:

  • 在迁移规划阶段划清责任边界:比如 Zabbix 只管物理层(主机、网络、硬件),Prometheus 只管应用层。一个指标只能有一个告警源。
  • 或者按告警严重级别分流:Zabbix 只发 P3 工单,Prometheus 发 P1/P2 电话告警。
  • 最糟糕的是”两套都发电话”——这是组织决策问题,不是工具问题。

8.2 标签维度爆炸

Prometheus 的每个 label 组合产生一条独立时间序列。如果一个指标有 5 个 label,每个 label 平均 10 个取值,总序列数是 10^5 = 10 万。如果其中一个 label 突然变成 user_id(千万级),总序列数瞬间到 10^12 级别,Prometheus 会 OOM、rule 评估变慢到几百秒。

典型的”杀手 label”:

  • user_id
  • request_id
  • email
  • ip(尤其是客户端 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 再打标签
  • pod_name 归一化:只保留 deployment 名,不保留随机后缀
  • 文档红线:团队规定不允许放进 Prometheus label 的字段列表

真正需要高基数维度的场景,应该用 Trace 或 Event,不要用 Metric。

8.3 Pull 模型在 NAT 后面失效

Prometheus pull 的前提是能连上目标的 IP:Port。在以下场景失效:

  • 目标在客户内网(SaaS 监控客户环境)
  • 目标是 Serverless 函数(没有稳定 IP)
  • 目标在 NAT 后面,IP 会变,端口会变

应对:

  • 用 Pushgateway(但有坑,见 8.4)
  • 用 VPN/隧道(Tailscale、WireGuard)把网络打平
  • 用 OTel Collector + OTLP push

8.4 Pushgateway 残留数据

Pushgateway 设计用于短生命周期 job,把最近一次的指标永久保存(直到下一次同 job 同 instance 的 push 覆盖它)。

问题:如果 job 不再运行了(被下线了、挂了),Pushgateway 会永远保留这次 push 的最后值。于是:

  • backup_last_success_unixtime 停留在很久以前的时间戳,但因为值没有变化,告警条件 time() - backup_last_success_unixtime > 86400 永远不触发(值不更新,采集到的 timestamp 是 Prometheus scrape 的时间,not push 的时间)。

解决:

  • TTL 管理:Pushgateway 没有内置 TTL,需要外部脚本定期 DELETE 长时间未更新的 group
  • push_time_seconds:Pushgateway 自动为每个 group 暴露一个 push_time_seconds 指标,告警条件用 time() - push_time_seconds{job="backup"} > 86400
  • 只在真正短生命周期 job 用,不要滥用成”应用推监控”的万能路径

8.5 时间戳精度不一致

Zabbix 存储精度到秒,Prometheus 精度到毫秒。如果你做跨系统关联分析——比如想把 Zabbix 的主机指标和 Prometheus 的应用指标放在同一张 Grafana 面板上按时间对齐——会发现 Zabbix 数据总是”整秒”,Prometheus 数据有毫秒抖动,join 时要么丢数据要么要做时间取整。

同样的问题在 Graphite(秒)和 Prometheus 之间也存在。

8.6 告警重复(与 8.1 不同的维度)

即使已经规划好责任边界,告警重复仍然会以其他方式出现:

  • 同一 Prometheus 规则分组配置不当group_by 配错,一条告警多条通知
  • 冗余 recording rule:一条底层 rule 和一条上层 rule 都触发告警
  • 跨集群同规则:多个 region 的 Prometheus 都有同名告警,Alertmanager 没有跨集群去重

Alertmanager 去重规则的配置非常重要:

route:
  group_by: [alertname, cluster, service]
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h

8.7 服务发现失效

Kubernetes SD 是 Prometheus 最常用的服务发现机制。典型的失效场景:

kubernetes_sd_configs:
  - role: pod
    namespaces:
      names:
        - production    # <-- 新上的 namespace 叫 prod-east-1,不在列表里

结果:新 namespace 的 Pod 完全不被发现,“静默漏监控”——Prometheus 上没有任何错误,Pod 暴露的 /metrics 仍然正常,但就是没人 scrape。

防御:

  • 不写死 namespace,用 label selector
  • 用一个 meta 监控项:absent(up{job="kubernetes-pods", namespace="prod-east-1"})
  • 在 CI 里校验新建 namespace 一定带某个标签

8.8 OTel Collector 上线没配 memory_limiter

Collector 默认没有内存限制。流量突增时 Collector OOM 崩溃,所有遥测数据同时中断(Metrics、Logs、Traces 都跟着一起挂)。

防御:永远在 pipeline 最前面配 memory_limiter processor:

processors:
  memory_limiter:
    check_interval: 1s
    limit_mib: 4000
    spike_limit_mib: 800

8.9 采样率不一致

如果 OTel Trace 在 SDK 侧做了 1% 的头采样(head-based sampling),Collector 里又加了 tail sampling policy,那么最终真正到达后端的 trace 比例可能远小于预期(1% × tail policy 命中率)。

规则是:头采样和尾采样二选一。头采样适合流量大、对完整性要求低的场景;尾采样适合要捕获所有错误 trace 的场景,但 Collector 需要缓存 decision_wait 时间内的所有 span,内存开销不小。

九、选型建议

基于上面所有分析,给出一组场景化建议。

9.1 纯新建系统

首选组合:OpenTelemetry + Prometheus(或 Mimir/VictoriaMetrics)+ Loki(或 ClickHouse)+ Tempo(或 Jaeger)+ Grafana。

理由:

  • OTel SDK 覆盖最全,Semantic Conventions 最标准
  • 三支柱统一数据管道
  • Grafana 作为可视化终端覆盖所有数据源
  • 工具链全开源,无 vendor lock-in

9.2 存量 Zabbix 系统

渐进式迁移

  1. 保留 Zabbix 做主机和网络监控(它在这件事上仍然很强)
  2. 新业务直接上 Prometheus + Grafana
  3. 老业务上 Prometheus exporter,逐步替换 Zabbix 的 application 监控
  4. 应用层 Trace 新加 OTel
  5. 告警规则逐步迁移,迁移期间严格分责

9.3 存量 Nagios

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

9.4 中小团队(< 50 人)

推荐 Grafana Cloud 免费额度:10k metrics series、50 GB logs、50 GB traces,对小团队够用几年。人力成本比运维开销更重要。

如果数据出海受限,自建一套 Prometheus + Loki + Tempo + Grafana 在单台中等规格机器上也能跑起来。

9.5 大团队(> 500 人)

自建:VictoriaMetrics(或 Mimir)+ Loki + Tempo + Grafana + OTel Collector Gateway 集群。

理由:

  • 成本规模化效应明显
  • 数据自主可控
  • 可以做定制告警、定制 UI
  • 团队具备足够的运维能力

9.6 决策表

场景 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 都在往这个方向走。

但是”更好的工具”永远不等于”更好的可观测性”。工具只能提供能力,真正决定可观测性水平的是:团队有没有把”每一个可疑事件都记录足够多上下文维度”这件事做成肌肉记忆。回到本文开头那句话——监控只能回答你已经想到的问题,可观测性的门票是承认你想不完所有问题。

参考资料

  1. Julius Volz, “Prometheus: Monitoring at SoundCloud”, SoundCloud Blog, 2012
  2. OpenTelemetry Project, “History of OpenTelemetry”, opentelemetry.io
  3. Ethan Galstad, Nagios Core Documentation, nagios.org
  4. Brendan Burns et al., Site Reliability Engineering, Google, 2016
  5. Charity Majors et al., Observability Engineering, O’Reilly, 2022
  6. 美团技术团队,《统一监控平台的架构演进》,美团技术博客,2019
  7. 腾讯云技术团队,《Prometheus 在腾讯云的落地实践》,腾讯云开发者社区,2020
  8. PromQL 官方文档,https://prometheus.io/docs/prometheus/latest/querying/basics/
  9. OpenTelemetry Collector 文档,https://opentelemetry.io/docs/collector/
  10. Graphite 官方文档,https://graphite.readthedocs.io/

上一篇可观测性全景:Metrics、Logs、Traces、Profiles、Events 五大支柱

下一篇指标体系设计:USE、RED、Golden Signals 与业务 KPI

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-22 · architecture / observability

可观测性工程

从 Metrics、Logs、Traces 到 Profiling、eBPF、OpenTelemetry 与 SLO 治理,面向中国工程团队的可观测性系统化手册。

2026-04-22 · architecture / observability

【可观测性工程】指标体系设计:USE、RED、Golden Signals 与业务 KPI

USE 方法论适用于资源,RED 方法论适用于请求,Golden Signals 适用于服务——三套方法论各有其适用对象。本文从 Brendan Gregg、Tom Wilkie、Google SRE 的原始定义出发,构建覆盖资源→服务→业务的完整指标体系,并给出 Prometheus 命名规范、基数治理策略与可抄的指标清单。