这两年,"本体论"突然又火了。Palantir Foundry 不把自己当又一个数据仓库,而是给企业建一套本体(Ontology):把散落在各系统里的表、流水、传感器,统一抽象成"客户""订单""设备",再定义关系和动作,让数据变成能被程序拿去推理的世界模型。大模型一来,这套说法更吃香——模型再强,也得先知道世界里有什么。国防、金融、制造都在谈。
那落到我们这行:APM 的本体到底是什么?建了它,故障定位就能解决吗?分开看。
问题一如何构建 APM 的本体模型APM 的本体就两步:从 Trace 提取实体和关系,再把它们 落地成指标表。用一个最小例子走一遍。
场景:服务 A 有两个实例 A-1、A-2;服务暴露两个接口;两个接口都调同一个数据库 DB、跑同一条 SQL1。上报 4 条 Trace:
| # | service | instance | endpoint | 调用 | sql |
|---|---|---|---|---|---|
| T1 | A | A-1 | 接口1 | DB | SQL1 |
| T2 | A | A-1 | 接口2 | DB | SQL1 |
| T3 | A | A-2 | 接口1 | DB | SQL1 |
| T4 | A | A-2 | 接口2 | DB | SQL1 |
第一步,提取。先看 T1:实体是服务 A、实例 A-1、接口1、数据库 DB、SQL1;关系是 A 有实例 A-1,A 暴露接口1,A-1 接到接口1 的请求,接口1 调用 DB,接口1 执行 SQL1,DB 拥有语句 SQL1。P99、耗时、调用次数不是实体,是挂在实体上的观测值。
4 条合在一起,实体和关系就是:
- Service hasInstance Instance:A → A-1、A-2
- Service exposes Endpoint:A → 接口1、接口2
- Instance serves request on Endpoint:A-1 / A-2 都会接到接口1、接口2 的请求
- Endpoint calls DB:接口1 → DB;接口2 → DB
- Endpoint executes SQL:接口1 → SQL1;接口2 → SQL1
- DB hasStatement SQL:DB → SQL1
第二步,落地。提取完,还要把一部分实体写成可查询的指标表。以 DataBuff 的 Doris 为例(这层 schema 已开源:github.com/databufflabs/databuff):表的维度列就是实体,call_count / resp_time 是观测值:
| 表 | 维度(实体) | 观测值(不是实体) | 对应哪些实体 |
|---|---|---|---|
| metric_service | service | call_count / resp_time / error_count | Service |
| metric_service_instance | service + instance | 同上 | Service、Instance |
| metric_service_http | service + instance + endpoint | 同上 | Service、Instance、Endpoint |
| metric_service_db | service + instance + db + sql | db 调用数 / db 耗时 | Service、Instance、DB、SQL |
上面 4 条 Trace 按这些维度一聚合,各表长这样:
- metric_service:按服务 A 汇总 → 1 行,入口 4 次(T1–T4)
- metric_service_instance:按实例拆 → 2 行,A-1 / A-2 各 2 次
- metric_service_http:按实例 + 接口拆 → 4 行,A-1·接口1、A-1·接口2、A-2·接口1、A-2·接口2 各 1 次
- metric_service_db:按实例 + 数据库 + SQL 拆 → 2 行,A-1·DB·SQL1、A-2·DB·SQL1 各 2 次
到这里,本体就建完了:Trace 里认出谁、谁连谁;再按这些实体落成指标表。
问题二只有本体,够吗?实体和关系建全了,排障是不是就通了?先看连接池日常怎么转:
就算本体里补上 ConnectionPool,写成 Service → ConnectionPool → Database,也只标得出图里那几个大方块。中间的分支和状态,多加一个实体盖不住——这一层就是逻辑:
- 获取连接:有空闲 → 返回;能扩容 → 新建;池满 → 锁等待;再等 → 超时抛错
- 使用 / 归还:执行 SQL → 归还 → 唤醒等待者
- 后台维护:心跳检测,剔除失效或空闲过久的连接
只认实体、不管逻辑,最常见的坑是接口变慢:
| 只看本体 | 带上逻辑 | |
|---|---|---|
| 推理 | 接口 → DB,接口慢 ⇒ DB 慢 | 卡在锁等待,SQL 还没跑 |
| 现象 | 接口耗时升 | DB 执行侧可能完全正常;超时更常表现为报错 |
| 结论 | 根因在 DB | 根因是等连接,不在执行 SQL |
第一性原理:程序 = 数据 + 逻辑。落到排障,两边分工不同:
| 本体(实体 + 关系) | 逻辑 | |
|---|---|---|
| 管什么 | 有什么、谁连谁 | 请求怎么走、卡在哪 |
| 锁什么 | 目标实体(接口1 / A-1 / DB) | 最后一公里根因(等连接 / 执行 SQL / 超时) |
| 缺了会怎样 | 不知道往哪下钻 | 停在「谁有问题」,到不了原因 |
实体关系好抽象,容易画成拓扑、落成指标表;连接池这类分支多半还在一线专家经验里,公开讨论少。若光掌握本体就够当一线专家,专家早该遍地都是——差的正是这层逻辑。
连接池只是一个切片:它说明「光认实体不够」,但还没回答——整应用的逻辑该怎么系统建模?不妨先看 DataBuff 怎么把「一个接口的耗时」拆开,再由此扩到整应用。
问题三如何为逻辑建模问题一里的「接口」,是入口的一种。DataBuff 对接口做逻辑建模时,不只给一条总响应时间,还按下游操作把耗时拆开——HTTP、RPC、DB、Redis、MQ,以及接口自身:
读图即可:入口平均约 240ms;分解里 HTTP service-b 约 100ms、RPC service-b 约 80ms,其余落在 MySQL、ES、Redis、Kafka 等。这就是对一个入口的逻辑建模——盯住入口的耗时与状态,再盯住入口之后每种操作的耗时与状态。
把范围扩到真实应用:一堆容器,对外提供 RPC / HTTP 等服务,同时依赖别的应用,以及 DB、Redis、MQ。逻辑侧要全面掌控健康,其实还是这两件事:
- 该应用所有入口的「耗时」和「状态」
- 每个入口之后每种操作的「耗时」和「状态」
入口不止 HTTP。常见还有:
- RPC 服务入口
- HTTP 服务入口
- MQ 消费消息入口
- 定时 Job 入口
- 其他入口
请求进了某个入口之后,后面大致会落到 5 种操作 + 1 种行为(通常还带机房维度)——上图里的分解项,就是这些操作在观测上的落地;问题一里的 DB / SQL,只是其中「DB 远程操作」这一行:
| 类型 | 关键属性(维度) |
|---|---|
| DB 远程操作 | dal group / table / operation(select、update、insert…)/ sql |
| Redis 远程操作 | command |
| MQ 远程操作(写消息) | exchange / routingKey / vhost |
| RPC 远程操作 | 依赖方服务 / remote method |
| Local 操作(除上述四种远程外的本地操作) | 暂无额外属性 |
| 抛出异常(行为) | 异常 name |
所以逻辑侧要落地统计的,就是:每个入口的耗时与状态,入口之后这 5 种操作的耗时与状态,以及异常次数——和问题一「按实体落成指标表」同构,只是维度从「谁」换成了「请求怎么走」。
对远程操作,光有总耗时还不够,最好再拆成三块:
- 客户端:建立连接、发送请求、接收响应
- 网络:客户端到服务端的传输
- 服务端:对端真正执行
有这三块,才能判断远程到底卡在哪。问题二的连接池就落在这里:池满锁等待,时间记在客户端,SQL 可能还没到服务端——所以「等连接 ≠ SQL 慢」。现实里三要素不一定采得全,但模型得先立住;采不全时,至少把「入口耗时」和「远端执行耗时」拆开,别糊成一个数。
模型立住之后,故障来了就可以按同一条链路往下问:
- 哪些入口受影响了?
- 受影响入口的本地操作是否异常?
- 受影响入口的哪些远程操作异常?具体到哪些操作属性(某张表、某条 SQL、某个 method…)?
- 是客户端 → 服务端网络出问题,还是服务端出问题?
- 受影响入口都抛出了哪些异常?
至此两边对齐了:本体回答「应用里有哪些实体、谁连谁」;逻辑模型回答「请求从哪个入口进来、后面经历了哪些操作、远程耗时卡在哪一段」。合在一起,才既找得到对象,也找得到原因。