数据模型与 Scope 体系
这一篇解决什么
前面把 Source/Metrics/Record 当抽象概念用,这一篇挖到底:Source 全家族 56 个子类逐个列字段、Scope 全表 98 个 scopeId 常量、Metrics 全家族 18 个聚合函数的 combine/calculate、DataTable 多值指标的哈希存储格式。读完这篇,OAL 里from(Service.latency).longAvg()这行字背后的每个零件都落到实处。
三层结构总览
数据模型分三层,靠两套注解驱动——@ScopeDeclaration 标 Source,@MetricsFunction 标 Metrics——AnnotationScan 启动时扫包注册。

每个概念的真实职责:
- Source 是从 Receiver 进入 OAP 的原始事实——一次请求的延迟、一次 GC 的耗时。还没被统计过。
- Scope 是给每个 Source 分配的整数 ID + 名字。OAL 里
from(Service).longAvg(),编译器靠 Scope 名找到对应的 Source 类。它本质是 Source 的类型标签,但用整数 ID 而不是类名,是为了在 OAL 编译产物和运行时分发里走最快的等值匹配路径。 - Catalog 比 Scope 粗。告警和 UI 渲染需要知道"这个指标挂在哪个拓扑节点上"——Service?Endpoint?Instance?Catalog 就是这个归属标记,一个 Catalog 下挂多个 Scope。
- Metrics 是 Source 聚合后的时间窗口统计值。每个子类的契约就两个方法:
combine()累加、calculate()终算。 - DataTable 是多值指标容器。普通 Metrics 一个时间窗口只存一个数;DataTable 用一个
HashMap<String,Long>同时存多个值,典型场景是一次算出 P50/P75/P90/P95/P99 五个分位数。
Source 全家族
Source 是什么
Source 是 OAP 分析管线的事实之源,每个子类对应一类可观测数据(一个服务、一个实例、一次 JVM GC、一次 DB 访问……),通过@ScopeDeclaration注册后被 OAL 引用。
为什么 Source 要分这么多子类
56 个 Source 子类不是过度设计,而是可观测域本身的形状决定的。一条请求进来,你想看服务级延迟(Service)、实例级延迟(ServiceInstance)、端点级延迟(Endpoint)、服务间调用关系(ServiceRelation)、JVM 内部状态、数据库访问、缓存访问、消息队列、eBPF 网络层、K8s 网格层……这些数据的字段集互不重叠:JVM GC 有 phase/time/count,数据库访问有 databaseTypeId/statement,缓存访问有 cacheTypeId/operation。硬塞一个 God Source 既丢类型安全,又让 OAL 编译器无法按字段名精准映射。
拆成 56 个子类的代价是注册成本,但被 AnnotationScan 启动期一次性扫描消化,运行期零开销。收益是 OAL 脚本里 from(Service.latency) 这类写法能被静态校验——latency 字段确实存在于 Service 类上,编译期就锁定。这就是 Source 多子类的根本理由:字段集的差异是天然的分类边界,用类型系统固化它。
ISource 接口的三个钩子
定义在 oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/ISource.java:
| 钩子 | 作用 | 行号 |
|---|---|---|
int scope() |
返回 scopeId | :31。编译期 OAL 用它匹配;运行期分发器用它找正确的 SourceDispatcher |
long getTimeBucket() / setTimeBucket() |
时间桶 | :33-35。所有 Source 都带时间维度,用 yyyMMddHHmm 形式的长整型表示一个时间窗口 |
String getEntityId() |
实体唯一标识 | :37。决定这条 Source 聚合到哪一行;同一 entityId 的 Source 合并到同一个 Metrics 行 |
default void prepare() |
分发前预处理 | :42。默认空实现,子类用来"用名字反算 ID" |
String toJson() |
手写 JSON 调试负载 | :55。手写而非反射序列化,用于 dsl-debug 捕获 |
prepare() 的典型用法
prepare() 在 SourceDispatcher.dispatch() 之前调用(ISource.java:40 的注释明确点出这个时序),核心作用是把"好读的 name"转成"好关联的 ID":
Endpoint.prepare()(Endpoint.java:131):serviceId = IDManager.ServiceID.buildId(serviceName, serviceLayer.isNormal())ServiceRelation.prepare()(ServiceRelation.java:110):同时算sourceServiceId和destServiceIdGenAIModelAccess.prepare()(GenAIModelAccess.java:68):算serviceId(用VIRTUAL_GENAI层)+entityId
Service、DatabaseAccess、MQAccess 这类"自己就是顶层实体"的 Source 没有 prepare(),而是把 ID 计算放在 getEntityId() 里做懒加载。区别在于:prepare 是分发前一次性补字段(适合一条 Source 关联多个实体的场景,如 ServiceRelation 要算两端),getEntityId 是用到才算(适合只算自身 ID 的场景)。
getEntityId() 的三种风格
| 风格 | 代表 | 实现 |
|---|---|---|
| 懒加载 + 缓存 | Service、ServiceInstance、ServiceRelation |
if (entityId == null) entityId = IDManager.xxx.buildId(...) |
| 直接返回 | ServiceInstanceJVMGC 等 JVM/CLR 类 |
return String.valueOf(id),用 Agent 上报的 instance id |
| 固定空串 | DatabaseSlowStatement |
return Const.EMPTY_STRING,每条慢 SQL 都是独立行,不按实体聚合 |
第三种是刻意为之:慢 SQL 不该被聚合,每条都该能在 UI 单独查到,所以 entityId 给空串,让每条 Source 落成独立行。
Source 子类全表(按类别分组)
下面覆盖 oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/ 下全部带 @ScopeDeclaration 的类。scopeId 常量来自 DefaultScopeDefine.java。
A. 拓扑核心类(Service / Instance / Endpoint 及其 Relation)
| 类名 | scopeId 常量 | 值 | catalog | 关键字段 |
|---|---|---|---|---|
Service |
SERVICE | 1 | SERVICE | name, layer, latency, status, httpResponseStatusCode, rpcStatusCode, type, tags, attr0~attr5 |
ServiceInstance |
SERVICE_INSTANCE | 2 | SERVICE_INSTANCE | serviceId, name, serviceName, serviceLayer, latency, status, sideCar |
Endpoint |
ENDPOINT | 3 | ENDPOINT | name, serviceId, serviceName, serviceLayer, latency, status, attr0~attr5 |
ServiceRelation |
SERVICE_RELATION | 4 | SERVICE_RELATION | sourceServiceId/Name/Layer, destServiceId/Name/Layer, endpoint, componentId, latency, detectPoint |
ServiceInstanceRelation |
SERVICE_INSTANCE_RELATION | 5 | SERVICE_INSTANCE_RELATION | source/dest 的 serviceId+instanceId+name+layer, endpoint, componentId, latency, detectPoint, tlsMode |
EndpointRelation |
ENDPOINT_RELATION | 6 | ENDPOINT_RELATION | endpoint, serviceId/Name, childEndpoint, childServiceId/Name, componentId, rpcLatency, detectPoint |
拓扑三件套 × 两层
这 6 个是 SkyWalking 最经典的"拓扑三件套 × 两层":每个维度(Service/Instance/Endpoint)既有自身指标,也有 Relation 描述调用关系。@ScopeDefaultColumn.VirtualColumnDefinition(fieldName="entityId", columnName="entity_id", isID=true)是这 6 个的统一主键声明——一个 entityId 列既是聚合 key 也是存储主键,Source 的多态主键设计在这里收口。
B. JVM 监控类(全部 catalog = SERVICE_INSTANCE)
| 类名 | scopeId 常量 | 值 | 关键字段 | 位置 |
|---|---|---|---|---|
ServiceInstanceJVMCPU |
SERVICE_INSTANCE_JVM_CPU | 8 | id, name, serviceName, serviceId, usePercent(double) | ServiceInstanceJVMCPU.java:28 |
ServiceInstanceJVMMemory |
SERVICE_INSTANCE_JVM_MEMORY | 9 | id, name, serviceId, heapStatus, init, max, used, committed | ServiceInstanceJVMMemory.java:28 |
ServiceInstanceJVMMemoryPool |
SERVICE_INSTANCE_JVM_MEMORY_POOL | 10 | id, name, serviceId, poolType(MemoryPoolType), init, max, used, committed | ServiceInstanceJVMMemoryPool.java:28 |
ServiceInstanceJVMGC |
SERVICE_INSTANCE_JVM_GC | 11 | id, name, serviceId, phase(GCPhase), time, count | ServiceInstanceJVMGC.java:28 |
ServiceInstanceJVMThread |
SERVICE_INSTANCE_JVM_THREAD | 33 | id, name, serviceId, liveCount, daemonCount, peakCount, runnable/blocked/waiting/timedWaiting 各状态线程数 | ServiceInstanceJVMThread.java:28 |
ServiceInstanceJVMClass |
SERVICE_INSTANCE_JVM_CLASS | 44 | 已加载/未加载类数 | ServiceInstanceJVMClass.java:28 |
JVM 类的共同模式
getEntityId()直接返回 Agent 上报的String.valueOf(id),prepare()不存在或仅设 serviceId,字段都是长整型数值。注意 scopeId 跳过了 7。
C. CLR(.NET)监控类(全部 catalog = SERVICE_INSTANCE)
| 类名 | scopeId 常量 | 值 | 关键字段 | 位置 |
|---|---|---|---|---|
ServiceInstanceCLRCPU |
SERVICE_INSTANCE_CLR_CPU | 19 | usePercent | ServiceInstanceCLRCPU.java:31 |
ServiceInstanceCLRGC |
SERVICE_INSTANCE_CLR_GC | 20 | gen0CollectCount, gen1CollectCount, gen2CollectCount, heapMemory | ServiceInstanceCLRGC.java:31 |
ServiceInstanceCLRThread |
SERVICE_INSTANCE_CLR_THREAD | 21 | 线程相关统计 | ServiceInstanceCLRThread.java:31 |
D. 数据库 / 缓存 / 消息队列 访问类
| 类名 | scopeId 常量 | 值 | catalog | 关键字段 |
|---|---|---|---|---|
DatabaseAccess |
DATABASE_ACCESS | 17 | SERVICE | name, databaseTypeId, latency, status |
DatabaseSlowStatement |
DATABASE_SLOW_STATEMENT | 18 | SERVICE | id, databaseServiceId, statement, latency, traceId, timestamp;getEntityId() 返回空串 |
ServiceDatabaseSlowStatement |
SERVICE_DATABASE_SLOW_STATEMENT | 91 | SERVICE | 服务维度的慢 SQL |
CacheAccess |
CACHE_ACCESS | 55 | SERVICE | name, cacheTypeId, latency, status, operation(VirtualCacheOperation) |
CacheSlowAccess |
CACHE_SLOW_ACCESS | 56 | SERVICE | 慢缓存访问 |
MQAccess |
MESSAGE_QUEUE_ACCESS | 63 | SERVICE | name, typeId, transmissionLatency, status, operation(MQOperation) |
MQEndpointAccess |
MESSAGE_QUEUE_ENDPOINT_ACCESS | 64 | ENDPOINT | MQ 的 endpoint 级访问 |
catalog 归属
DatabaseAccess/CacheAccess/MQAccess的 catalog 都是SERVICE,在告警/UI 里归到"服务"维度;MQEndpointAccess归到ENDPOINT维度。这决定了 UI 拓扑图上它们挂哪个节点。
E. GenAI 类(AI 模型观测,10.x 引入)
| 类名 | scopeId 常量 | 值 | catalog | 关键字段 |
|---|---|---|---|---|
GenAIProviderAccess |
GEN_AI_PROVIDER_ACCESS | 96 | SERVICE | name, inputTokens, outputTokens, totalEstimatedCost, latency, status |
GenAIModelAccess |
GEN_AI_MODEL_ACCESS | 97 | SERVICE_INSTANCE | serviceName, serviceId, modelName, inputTokens, outputTokens, totalEstimatedCost, timeToFirstToken, latency, status |
GenAI cost 单位
GenAIMetrics.java:24是普通 POJO(@Data,无@ScopeDeclaration),不是 Source,用作中间数据载体。注释说明 cost 单位是 1e-6 货币(micro-USD)——避免浮点累加误差,所有 cost 用 long 存,展示时再除以 1e6。
F. eBPF / Cilium / Envoy 类
Cilium 类全部继承抽象基类 CiliumMetrics(CiliumMetrics.java:24),后者预定义了 verdict(forwarded/dropped)、type(tcp/http/dns/kafka)、direction(ingress/egress)、dropReason 以及内嵌的 HTTPMetrics/KafkaMetrics/DNSMetrics 子结构。抽象基类把 6 个 Cilium Source 的公共字段抽出来,避免 6 份重复声明。
| 类名 | scopeId 常量 | 值 | catalog | 位置 |
|---|---|---|---|---|
CiliumService |
CILIUM_SERVICE | 78 | SERVICE | CiliumService.java:30 |
CiliumServiceInstance |
CILIUM_SERVICE_INSTANCE | 79 | SERVICE_INSTANCE | CiliumServiceInstance.java:30 |
CiliumServiceRelation |
CILIUM_SERVICE_RELATION | 80 | SERVICE_RELATION | CiliumServiceRelation.java:30 |
CiliumServiceInstanceRelation |
CILIUM_SERVICE_INSTANCE_RELATION | 81 | SERVICE_INSTANCE_RELATION | CiliumServiceInstanceRelation.java:30 |
CiliumEndpoint |
CILIUM_ENDPOINT | 82 | ENDPOINT | CiliumEndpoint.java:30 |
CiliumEndpointRelation |
CILIUM_ENDPOINT_REALATION | 83 | ENDPOINT_RELATION | CiliumEndpointRelation.java:30(注:常量名源码即拼成 REALATION) |
EnvoyInstanceMetric |
ENVOY_INSTANCE_METRIC | 22 | SERVICE_INSTANCE | EnvoyInstanceMetric.java:33 |
eBPF Profiling 数据类(非观测指标,是 profiling 明细):
| 类名 | scopeId 常量 | 值 | 位置 |
|---|---|---|---|
EBPFProfilingData |
EBPF_PROFILING_DATA | 48 | EBPFProfilingData.java:29 |
EBPFProcessProfilingSchedule |
EBPF_PROFILING_SCHEDULE | 47 | EBPFProcessProfilingSchedule.java:31 |
JFRProfilingData |
JFR_PROFILING_DATA | 84 | JFRProfilingData.java:30 |
PprofProfilingData |
PPROF_PROFILING_DATA | 93 | PprofProfilingData.java:28 |
G. K8s 类(rover 采集的 mesh/网络层数据)
K8s 类全部继承抽象基类 K8SMetrics(K8SMetrics.java:24),后者预定义了 connect/accept/close/write/read 五种网络操作,每种再分 L2/L3/L4 三层 + Protocol(HTTP) 嵌套结构。
| 类名 | scopeId 常量 | 值 | catalog | 位置 |
|---|---|---|---|---|
K8SService |
K8S_SERVICE | 72 | SERVICE | K8SService.java:34 |
K8SServiceInstance |
K8S_SERVICE_INSTANCE | 73 | SERVICE_INSTANCE | K8SServiceInstance.java:30 |
K8SServiceRelation |
K8S_SERVICE_RELATION | 74 | SERVICE_RELATION | K8SServiceRelation.java:34 |
K8SServiceInstanceRelation |
K8S_SERVICE_INSTANCE_RELATION | 75 | SERVICE_INSTANCE_RELATION | K8SServiceInstanceRelation.java:30 |
K8SEndpoint |
K8S_ENDPOINT | 76 | ENDPOINT | K8SEndpoint.java:30 |
K8s 和 Cilium 复用拓扑 catalog
K8s 和 Cilium 复用了同一套拓扑 catalog(SERVICE/SERVICE_INSTANCE/…),能在 UI 上和普通 Service 拓扑混排。两者都带attr0~attr5透传属性。复用 catalog 是刻意的——网络层和 mesh 层最终都要在服务拓扑图上呈现,共用 catalog 让 UI 不必为每层单独写渲染逻辑。
H. Process 类(进程级,eBPF 之上)
| 类名 | scopeId 常量 | 值 | catalog | 关键字段 | 位置 |
|---|---|---|---|---|---|
Process |
PROCESS | 45 | PROCESS | instanceId, serviceId, name, serviceName, instanceName, agentId, detectType, labels, profilingSupportStatus | Process.java:33 |
ProcessRelation |
PROCESS_RELATION | 54 | PROCESS_RELATION | 进程间调用关系 | ProcessRelation.java:30 |
I. Log / Event / Segment 等明细类
| 类名 | scopeId 常量 | 值 | 说明 | 位置 |
|---|---|---|---|---|
Segment |
SEGMENT | 12 | 链路分段原始数据:segmentId, traceId, serviceId, serviceInstanceId, endpointId, startTime, latency, isError, dataBinary, tags | Segment.java:32 |
Log |
LOG | 41 | 继承 AbstractLog:uniqueId, timestamp, serviceId, serviceInstanceId, endpointId, traceId, traceSegmentId, spanId, contentType, content, error |
Log.java:23 |
这些是明细 Record
Segment、Log、DatabaseSlowStatement是明细/Record 性质的 Source,@ScopeDeclaration没有 catalog(告警不基于它们),它们走 LAL 脚本或硬编码 dispatcher 转成 Record 落库。
J. 元数据 / 配置类(无 catalog,不参与指标告警)
| 类名 | scopeId 常量 | 值 | 作用 |
|---|---|---|---|
ServiceMeta |
SERVICE_META | 29 | 只含 name + layer,用于服务元数据更新 |
ServiceInstanceUpdate |
SERVICE_INSTANCE_UPDATE | 30 | 实例元数据更新 |
EndpointMeta |
ENDPOINT_META | 42 | 端点元数据 |
ServiceLabel |
SERVICE_LABEL | 49 | 给服务打标签:serviceId, serviceName, label |
TagAutocomplete |
TAG_AUTOCOMPLETE | 50 | tag 自动补全索引 |
K. TCP / 非标拓扑类
| 类名 | scopeId 常量 | 值 | catalog |
|---|---|---|---|
TCPService |
TCP_SERVICE | 57 | SERVICE |
TCPServiceInstance |
TCP_SERVICE_INSTANCE | 58 | SERVICE_INSTANCE |
TCPServiceRelation |
TCP_SERVICE_RELATION | 59 | SERVICE_RELATION |
TCPServiceInstanceRelation |
TCP_SERVICE_INSTANCE_RELATION | 60 | SERVICE_INSTANCE_RELATION |
TCPServiceInstanceUpdate |
TCP_SERVICE_INSTANCE_UPDATE | 61 | (无) |
范围说明
上面 A-K 覆盖server-core/source/下全部 56 个带@ScopeDeclaration的类。其余 scopeId(BROWSER_、ZIPKIN_、PROFILE_、UI_、ALARM、EVENT、SPAN_ATTACHED_EVENT、CONTINUOUS_PROFILING_POLICY、RUNTIME_RULE、ALARM_RECOVERY、PPROF_TASK、ASYNC_PROFILER_* 等)的 Source 类位于server-core其它子包(manual/generic/browser/zipkin/profile 等),不在source/目录内,注册机制完全相同(都靠AnnotationScan扫@ScopeDeclaration)。
SourceDecorator 机制
Decorator 是什么
Decorator 是 OAL 在 Source 进入流处理前补字段的扩展点,典型用途是把Layer名写到attr0,让生成的指标带一个可查询的属性列。
接口 oap-server/server-core/.../analysis/ISourceDecorator.java:27:
public interface ISourceDecorator<SOURCE extends ISource> {
int getSourceScope(); // 声明装饰哪个 scope 的 Source
void decorate(SOURCE source); // 补字段,如 set attr0
}
SourceDecoratorManager(analysis/SourceDecoratorManager.java:31)维护静态 DECORATOR_MAP,key 是 decorator 类的 getSimpleName()。addIfAsSourceDecorator() 通过反射拿到泛型参数 <SOURCE>,校验它是 ISource 子类后实例化注册。
全仓库目前只有 3 个 ISourceDecorator 实现:
| Decorator | 装饰的 Source | 行为 | 位置 |
|---|---|---|---|
ServiceDecorator |
Service | setAttr0(layer.name()) |
source/ServiceDecorator.java:23 |
EndpointDecorator |
Endpoint | setAttr0(serviceLayer.name()) |
source/EndpointDecorator.java:23 |
K8SServiceDecorator |
K8SService | 设置 attr0(layer 相关) | source/K8SServiceDecorator.java |
调用方式是 Source 上的 decorate(String decoratorName) 方法(见 Service.java:124、Endpoint.java:125),按类名从 DECORATOR_MAP 取出对应 decorator 执行。
ScopeDefaultColumn.DefinedByField 注解的 isAttribute=true(ScopeDefaultColumn.java:77,since 10.2.0)正是为这些 attr 字段标记"这是 decorator 写的属性列",用于查询条件。
Scope 体系完整
为什么 Scope 用整数 ID 分区
scopeId 是整数而不是字符串,不是省内存的小技巧,是 OAP 性能链路上的一个关键决策。Source 进入流处理后,SourceDispatcher 要按 scope 把 Source 路由到正确的处理器——这是一个每条数据都要走的热路径。用整数做 Map<Integer,?> 的 key,hash 和等值比较都是 O(1) 的原生操作;若用字符串 scope 名,每次路由都要 String.equals 走字符比较,在高 TPS 下不可忽略。
更关键的是 OAL 编译产物:from(Service).longAvg() 编译后,Service 这个名字被换成它的 scopeId 常量 1,后续所有匹配都是 int 等值。这让 OAL 的运行时和 Source 分发器共享同一套整数契约,字符串只在启动期注册阶段出现一次。
DefaultScopeDefine.java:36-40 的注释把分区规则写死了:
All metrics IDs in [0, 10,000) are reserved in Apache SkyWalking. If you want to extend the scope, recommend to start with 10,000.
0~9999 是官方保留区,第三方扩展从 10000 开始。整数空间足够大,官方用前 100 个,留 9900 个给扩展,且 addNewScope() 会做冲突校验(DefaultScopeDefine.java:217-231):id 重复抛 UnexpectedException,id 为负抛异常,name 重复抛异常。这套校验保证注册表的唯一性,不靠人工纪律。
DefaultScopeDefine 全部 scopeId 常量
文件:oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/DefaultScopeDefine.java。下表按文件出现顺序列出(@since/@Deprecated 直接来自源码):
| 常量名 | 值 | 备注 | 行号 |
|---|---|---|---|
UNKNOWN |
0 | since 9.0.0 | :45 |
ALL |
0 | @Deprecated from 9.0.0 | :50 |
SERVICE |
1 | :51 |
|
SERVICE_INSTANCE |
2 | :52 |
|
ENDPOINT |
3 | :53 |
|
SERVICE_RELATION |
4 | :54 |
|
SERVICE_INSTANCE_RELATION |
5 | :55 |
|
ENDPOINT_RELATION |
6 | :56 |
|
SERVICE_INSTANCE_JVM_CPU |
8 | 注意:7 跳过 | :57 |
SERVICE_INSTANCE_JVM_MEMORY |
9 | :58 |
|
SERVICE_INSTANCE_JVM_MEMORY_POOL |
10 | :59 |
|
SERVICE_INSTANCE_JVM_GC |
11 | :60 |
|
SEGMENT |
12 | :61 |
|
ALARM |
13 | :62 |
|
DATABASE_ACCESS |
17 | :63 |
|
DATABASE_SLOW_STATEMENT |
18 | :64 |
|
SERVICE_INSTANCE_CLR_CPU |
19 | :65 |
|
SERVICE_INSTANCE_CLR_GC |
20 | :66 |
|
SERVICE_INSTANCE_CLR_THREAD |
21 | :67 |
|
ENVOY_INSTANCE_METRIC |
22 | :68 |
|
ZIPKIN_SPAN |
23 | :69 |
|
JAEGER_SPAN |
24 | @Deprecated | :71 |
HTTP_ACCESS_LOG |
25 | @Deprecated | :73 |
PROFILE_TASK |
26 | :74 |
|
PROFILE_TASK_LOG |
27 | :75 |
|
PROFILE_TASK_SEGMENT_SNAPSHOT |
28 | :76 |
|
SERVICE_META |
29 | :77 |
|
SERVICE_INSTANCE_UPDATE |
30 | :78 |
|
NETWORK_ADDRESS_ALIAS |
31 | :79 |
|
UI_TEMPLATE |
32 | :80 |
|
SERVICE_INSTANCE_JVM_THREAD |
33 | :81 |
|
BROWSER_ERROR_LOG |
34 | browser 段开始 | :84 |
BROWSER_APP_PERF |
35 | :85 |
|
BROWSER_APP_PAGE_PERF |
36 | :86 |
|
BROWSER_APP_SINGLE_VERSION_PERF |
37 | :87 |
|
BROWSER_APP_TRAFFIC |
38 | :88 |
|
BROWSER_APP_SINGLE_VERSION_TRAFFIC |
39 | :89 |
|
BROWSER_APP_PAGE_TRAFFIC |
40 | :90 |
|
LOG |
41 | :92 |
|
ENDPOINT_META |
42 | :93 |
|
EVENT |
43 | :95 |
|
SERVICE_INSTANCE_JVM_CLASS |
44 | :97 |
|
PROCESS |
45 | :99 |
|
EBPF_PROFILING_TASK |
46 | :100 |
|
EBPF_PROFILING_SCHEDULE |
47 | :101 |
|
EBPF_PROFILING_DATA |
48 | :102 |
|
SERVICE_LABEL |
49 | :103 |
|
TAG_AUTOCOMPLETE |
50 | :104 |
|
ZIPKIN_SERVICE |
51 | :105 |
|
ZIPKIN_SERVICE_SPAN |
52 | :106 |
|
ZIPKIN_SERVICE_RELATION |
53 | :107 |
|
PROCESS_RELATION |
54 | :108 |
|
CACHE_ACCESS |
55 | :109 |
|
CACHE_SLOW_ACCESS |
56 | :110 |
|
TCP_SERVICE |
57 | :112 |
|
TCP_SERVICE_INSTANCE |
58 | :113 |
|
TCP_SERVICE_RELATION |
59 | :114 |
|
TCP_SERVICE_INSTANCE_RELATION |
60 | :115 |
|
TCP_SERVICE_INSTANCE_UPDATE |
61 | :116 |
|
SAMPLED_SLOW_TRACE |
62 | :117 |
|
MESSAGE_QUEUE_ACCESS |
63 | :119 |
|
MESSAGE_QUEUE_ENDPOINT_ACCESS |
64 | :120 |
|
SPAN_ATTACHED_EVENT |
65 | :122 |
|
SAMPLED_STATUS_4XX_TRACE |
66 | :123 |
|
SAMPLED_STATUS_5XX_TRACE |
67 | :124 |
|
CONTINUOUS_PROFILING_POLICY |
68 | :126 |
|
UI_MENU |
69 | :128 |
|
SERVICE_HIERARCHY_RELATION |
70 | 服务层级关系 | :130 |
INSTANCE_HIERARCHY_RELATION |
71 | 实例层级关系 | :131 |
K8S_SERVICE |
72 | :133 |
|
K8S_SERVICE_INSTANCE |
73 | :134 |
|
K8S_SERVICE_RELATION |
74 | :135 |
|
K8S_SERVICE_INSTANCE_RELATION |
75 | :136 |
|
K8S_ENDPOINT |
76 | :137 |
|
CILIUM_SERVICE |
78 | 注意:77 跳过 | :139 |
CILIUM_SERVICE_INSTANCE |
79 | :140 |
|
CILIUM_SERVICE_RELATION |
80 | :141 |
|
CILIUM_SERVICE_INSTANCE_RELATION |
81 | :142 |
|
CILIUM_ENDPOINT |
82 | :143 |
|
CILIUM_ENDPOINT_REALATION |
83 | 源码常量名即此拼写 | :144 |
JFR_PROFILING_DATA |
84 | :146 |
|
ASYNC_PROFILER_TASK |
85 | :147 |
|
ASYNC_PROFILER_TASK_LOG |
86 | :148 |
|
BROWSER_APP_WEB_VITALS_PAGE_PERF |
87 | :150 |
|
BROWSER_APP_RESOURCE_PERF |
88 | :151 |
|
BROWSER_APP_WEB_INTERACTION_PAGE_PERF |
89 | :152 |
|
SW_SPAN_ATTACHED_EVENT |
90 | :153 |
|
SERVICE_DATABASE_SLOW_STATEMENT |
91 | :154 |
|
PPROF_TASK |
92 | :155 |
|
PPROF_PROFILING_DATA |
93 | :156 |
|
PPROF_TASK_LOG |
94 | :157 |
|
ALARM_RECOVERY |
95 | :158 |
|
GEN_AI_PROVIDER_ACCESS |
96 | :159 |
|
GEN_AI_MODEL_ACCESS |
97 | :160 |
|
RUNTIME_RULE |
98 | :161 |
跳过的 ID
7、14、15、16、77 在源码中无对应常量。这些是历史废弃或预留——addNewScope()的冲突校验只挡重复,不挡空缺,所以跳号不会报错,只是 ID 空间留白。
ScopeDeclaration 注解
定义在 source/ScopeDeclaration.java:43:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ScopeDeclaration {
int id(); // 对应 DefaultScopeDefine 里的常量值
String name(); // scope 名字,如 "Service",OAL 脚本里用这个名字
String catalog() default ""; // 顶层 catalog 名,空串表示不参与告警
}
catalog 字段(:53-55)注释明确:当 scope 不是 metric,或其生成的 metrics 不支持告警时,留空。这就是 Segment/Log/元数据类没有 catalog 的注解层依据。
Catalog 机制
Catalog 是什么
Catalog 是告警/UI 的维度归属标签。告警规则需要知道一个指标挂在哪个拓扑节点上才能在 UI 渲染——inServiceCatalog(scopeId)返回 true 就说明这条指标能在服务拓扑图上显示。
DefaultScopeDefine.java:166-182 定义了 8 个 catalog 名字常量和 8 个对应的 Map<Integer,Boolean> catalog 表。注意是 8 张独立的 Map 而不是一张大 Map——查 catalog 时只需 SERVICE_CATALOG.containsKey(scopeId),O(1),不必遍历。
| catalog 名常量 | 字符串值 | 含义 | 典型成员 scope |
|---|---|---|---|
SERVICE_CATALOG_NAME |
“SERVICE” | 服务维度 | Service(1), DatabaseAccess(17), CacheAccess(55), MQAccess(63), K8SService(72), CiliumService(78), TCPService(57), GenAIProviderAccess(96) |
SERVICE_INSTANCE_CATALOG_NAME |
“SERVICE_INSTANCE” | 实例维度 | ServiceInstance(2), 所有 JVM/CLR 类, Envoy(22), K8SServiceInstance(73), CiliumServiceInstance(79), GenAIModelAccess(97) |
ENDPOINT_CATALOG_NAME |
“ENDPOINT” | 端点维度 | Endpoint(3), MQEndpointAccess(64), K8SEndpoint(76), CiliumEndpoint(82) |
SERVICE_RELATION_CATALOG_NAME |
“SERVICE_RELATION” | 服务关系 | ServiceRelation(4), K8SServiceRelation(74), CiliumServiceRelation(80), TCPServiceRelation(59) |
SERVICE_INSTANCE_RELATION_CATALOG_NAME |
“SERVICE_INSTANCE_RELATION” | 实例关系 | ServiceInstanceRelation(5), K8s/Cilium/TCP 对应关系 |
ENDPOINT_RELATION_CATALOG_NAME |
“ENDPOINT_RELATION” | 端点关系 | EndpointRelation(6), CiliumEndpointRelation(83) |
PROCESS_CATALOG_NAME |
“PROCESS” | 进程 | Process(45) |
PROCESS_RELATION_CATALOG_NAME |
“PROCESS_RELATION” | 进程关系 | ProcessRelation(54) |
查询 API:catalogOf(int scope)(:422)返回该 scope 所属 catalog 名,找不到返回 “ALL”;inServiceCatalog/inServiceInstanceCatalog/inEndpointCatalog/...(:342-414)判断 scopeId 是否属于某 catalog。
AnnotationScan 如何扫描注册
文件:oap-server/server-core/.../annotation/AnnotationScan.java。

机制(AnnotationScan.java:53-69):
ClassPath.from(classLoader).getTopLevelClassesRecursive("org.apache.skywalking")——递归扫描org.apache.skywalking包下所有顶层类(:55)。用 ClassPath API 而不是自写文件遍历,是因为 SkyWalking 可能打成 fat jar 或被 OSGi 容器加载,ClassPath API 能正确处理这些部署形态。- 对每个类,遍历已注册的
AnnotationListener,若类上有该 listener 关心的注解则加入缓存(:59-62)。 - 全部扫完后,按类名排序,逐个回调
listener.notify(aClass)(:66-68,排序见:89)。排序这一步不是洁癖——DefaultScopeDefine的注册会写静态 Map,排序保证相同 scopeId 的冲突抛错信息在不同机器上可复现,便于排查。
DefaultScopeDefine.Listener(:194-207)就是这样一个 listener:annotation() 返回 ScopeDeclaration.class,notify() 调 addNewScope(declaration, originalClass)。
addNewScope()(:215-297)做四件事:
- 冲突校验 + 把
id→name、name→id存入ID_2_NAME/NAME_2_ID(:233-234),建立双向查找表。 - 扫描类上的
@ScopeDefaultColumn.VirtualColumnDefinition(类级注解,定义主键列)和字段上的@ScopeDefaultColumn.DefinedByField,收集成ScopeDefaultColumn列表存入SCOPE_COLUMNS(:236-268)。这一步把 Source 的"默认列"传递给后续生成的 Metrics 实体——Metrics 继承 Source 的列,这样查询时能按 Source 的字段过滤。 - 根据
catalog()把 scopeId 归入对应 catalog 表(:270-296)。 - 字段上若标了
@ScopeDefaultColumn.BanyanDB(shardingKeyIdx=N),记录分片键索引(:252-254),供 BanyanDB 存储层做分片。
ScopeDefaultColumn(source/ScopeDefaultColumn.java:34)承载这些列定义,其中 requireDynamicActive=true 表示只有当 core/activeExtraModelColumns=true 时才真正加到生成的指标列里(:62-64),用于按需开启冗余列以省存储。
Metrics 子类家族
Metrics 是什么
Metrics 是对 Source 聚合后的时间窗口统计值,每个子类封装一种聚合算法(求和/平均/分位数/直方图……),通过@MetricsFunction注解暴露给 OAL,核心契约是combine()累加 +calculate()终算。
为什么 combine 和 calculate 要分两步
这是 SkyWalking 流处理模型的核心设计,不是工程洁癖。一条 Source 进来时,你不知道它是不是这个时间窗口的最后一条——窗口可能还没结束,实例可能还在不断上报。所以 combine() 只做可合并的中间态累加:LongAvg 存 summation 和 count 而不是 value,因为 summation 和 count 在多实例合并时各自相加结果正确,而 value = avg1 + avg2 是错的。
calculate() 在窗口结束或降采样时才调,这时所有中间态都齐了,做一次除法产出最终值。这个分工让跨实例合并变得自然:实例 A 的 (sum=100, count=10) 和实例 B 的 (sum=200, count=20) 合并成 (sum=300, count=30),最后 calculate 得到 value=10。如果一开始就存 value,A 是 10、B 是 10,合并后还是 10,但真实平均是 10——这次碰巧对,但只要 count 不同就错。
不是所有 Metrics 都需要两步:CountMetrics、SumMetrics、MaxLongMetrics 的 calculate() 是空操作,因为它们的中间态就是终态。但契约统一保留两步,让流处理框架不用关心具体子类。
Metrics 基类契约
文件:oap-server/server-core/.../analysis/metrics/Metrics.java。Metrics 继承 StreamData 并实现 StorageData, ToJson(:41)。核心抽象方法:
| 方法 | 作用 | 行号 |
|---|---|---|
boolean combine(Metrics metrics) |
合并同类型 metrics;返回 true 表示继续处理,false 表示丢弃 | :68 |
void calculate() |
时间窗口结束时计算最终值 | :73 |
Metrics toHour() / toDay() |
降采样到小时/天 | :80,87 |
StorageID id0() |
生成存储唯一 ID(基类 id() 做缓存) |
:167 |
时间相关:
timeBucket字段(:49)+lastUpdateTimestamp(:59):用于 session cache 过期判断isExpired(timestamp, expiredThreshold)(:103)。toTimeBucketInHour()(:107):分钟桶/100转小时桶;toTimeBucketInDay()(:115):分钟桶/10000或小时桶/100。getDurationInMinute()(:128):分钟桶返回 1,小时桶 60,天桶 1440——这是CPMMetrics算"每分钟次数"的依据。
OAL 注解体系(Metrics 如何被 OAL 引用)
每个 Metrics 子类用 @MetricsFunction(functionName="xxx") 声明 OAL 函数名(如 count/longAvg/percentile2)。combine 方法上还用一组参数注解告诉 OAL 编译器"这个参数从哪来":
| 注解 | 含义 |
|---|---|
@Entrance |
标记这是 OAL 入口 combine 方法 |
@SourceFrom |
参数来自 Source 的某字段 |
@ConstOne |
参数是常量 1(每来一条 Source 计 1) |
@Arg |
参数是 OAL 调用者传的参数 |
@Expression |
参数是 OAL 表达式(boolean) |
@DefaultValue |
给 @Arg 一个默认值 |
例如 LongAvgMetrics.combine(@SourceFrom long summation, @ConstOne long count)(LongAvgMetrics.java:57)表示"summation 取自 Source 的 latency,count 每次计 1"。OAL 编译器读这些注解,把 from(Service.latency).longAvg() 翻译成"取 Source.latency 字段传给 summation,count 固定传 1"。
全部 Metrics 子类(18 个函数)
| 类名 | OAL 函数名 | 聚合语义 | combine 算法 | calculate 算法 | 值类型 | 位置 |
|---|---|---|---|---|---|---|
CountMetrics |
count |
计数 | value += count(@ConstOne) |
空操作 | long | CountMetrics.java:32 |
SumMetrics |
sum |
求和 | value += count(@SourceFrom) |
空操作 | long | SumMetrics.java:32 |
LongAvgMetrics |
longAvg |
长整型平均 | summation += s; count += 1 |
value = summation / count |
long | LongAvgMetrics.java:33 |
DoubleAvgMetrics |
doubleAvg |
浮点平均 | summation += s; count += 1 |
value = summation / count |
double | DoubleAvgMetrics.java:33 |
PercentMetrics |
percent |
百分比 | match 命中则 match++; total++ |
percentage = match*10000/total |
int | PercentMetrics.java:32 |
RateMetrics |
rate |
比率 | 两个 @Expression boolean 分别累加 numerator/denominator | percentage = numerator*10000/denominator |
int | RateMetrics.java:31 |
CPMMetrics |
cpm |
每分钟调用数 | total += count |
value = total / getDurationInMinute() |
long | CPMMetrics.java:32 |
ApdexMetrics |
apdex |
Apdex 满意度 | 按 t(阈值)/4t 分档累加 sNum/tNum/totalNum | value = (sNum*10000 + tNum*10000/2) / totalNum |
int | ApdexMetrics.java:41 |
MaxLongMetrics |
max |
长整型最大值 | if (count > value) value = count |
空操作 | long | MaxLongMetrics.java:32 |
MinLongMetrics |
min |
长整型最小值 | if (count < value) value = count |
空操作 | long | MinLongMetrics.java |
MaxDoubleMetrics |
maxDouble |
浮点最大值 | 同 max | 空操作 | double | MaxDoubleMetrics.java |
MinDoubleMetrics |
minDouble |
浮点最小值 | 同 min,初值 Double.MAX_VALUE |
空操作 | double | MinDoubleMetrics.java:32 |
PercentileMetrics |
percentile |
多分位数(旧) | dataset.valueAccumulation(value/precision, 1) |
排序后按 roof 取 P50/75/90/95/99 | int[] (MultiIntValuesHolder) | PercentileMetrics.java:44(@Deprecated) |
PercentileMetrics2 |
percentile2 |
多分位数(新) | 同上 | 同上,但结果存为 {p=50},v1\\\|{p=75},v2... 的 DataTable |
DataTable (LabeledValueHolder) | PercentileMetrics2.java:43 |
HistogramMetrics |
histogram |
直方图/热力图 | dataset.valueAccumulation(index*step, 1) |
空操作(无单值) | 无(用 dataset) | HistogramMetrics.java:39 |
LabelCountMetrics |
labelCount |
按标签计数 | dataset.valueAccumulation(label, count, maxLabelCount) |
把 dataset 转 {n=label},count 的 value DataTable |
DataTable | LabelCountMetrics.java:33 |
LabelAvgMetrics |
labelAvg |
按标签平均 | summation.valueAccumulation(label,count); count.valueAccumulation(label,1) |
value = {n=label} : summation/count |
DataTable | LabelAvgMetrics.java:34 |
另有辅助类型:
LongValueHolder/DoubleValueHolder/IntValueHolder(单值接口)、MultiIntValuesHolder(多 int 值)、LabeledValueHolder(返回 DataTable)、DataLabel(标签 map)。
三个典型 combine/calculate 实现详解
1) LongAvgMetrics —— 最经典的"先存中间态、终算时做除法"
// LongAvgMetrics.java:57-72
@Entrance
public final void combine(@SourceFrom long summation, @ConstOne long count) {
this.summation += summation; // 累加被求和字段
this.count += count; // 累加计数
}
public final boolean combine(Metrics metrics) { // 跨实例合并
LongAvgMetrics m = (LongAvgMetrics) metrics;
combine(m.summation, m.count);
return true;
}
public final void calculate() { // 窗口终算
this.value = this.summation / this.count;
}
summation 和 count 标了 @Column(storageOnly=true)(:41,46,不建索引只存值),value 标 @Column(dataType=COMMON_VALUE)(:52,可查询的最终值)。combine 阶段只累加中间态,calculate 阶段才做除法——这样多实例合并时先把 sum 和 count 各自加起来,最后再除,结果正确。
2) PercentileMetrics2 —— 用 DataTable 算多分位数
// PercentileMetrics2.java:81-128
@Entrance
public final void combine(@SourceFrom int value, @Arg int precision) {
String index = String.valueOf(value / precision); // 把 latency 按 precision 分桶
dataset.valueAccumulation(index, 1L); // 该桶计数 +1
}
public boolean combine(Metrics metrics) {
this.dataset.append(percentileMetrics.dataset); // DataTable 直接合并
return true;
}
public final void calculate() {
long total = dataset.sumOfValues();
int[] roofs = {Math.round(total*50/100), ...99}; // 每个 rank 的累计计数阈值
// 按 key(桶下标)升序累加,达到某 rank 的 roof 就记录该分位数值
percentileValues.put(label.toString(), Long.parseLong(key) * precision);
}
RANKS = {50,75,90,95,99}(:48),一次 combine 同时算 5 个分位数。算法本质是桶计数 + 累加定位:每条 latency 除以 precision 落到某个桶,calculate 时把所有桶按 key 升序累加,累加值首次达到 total * p/100 的那个桶,其桶下标乘 precision 就是该分位数的近似值。precision 越小越精确,但桶数越多。
结果用 DataLabel{p=50} 作 key 存入 percentileValues 这个 DataTable,存储格式为 {p=50},value1|{p=75},value2|...(注释 :37-39)。
3) ApdexMetrics —— 带"档位"的满意度评分
// ApdexMetrics.java:75-100
@Entrance
public final void combine(@SourceFrom int value, @Arg String name, @Arg boolean status) {
int t = DICT.lookup(name).intValue(); // 从 ConfigurationDictionary 取阈值 T
int t4 = t * 4;
totalNum++;
if (!status || value > t4) return; // 失败或 >4T 不计入满意
if (value > t) tNum++; // (T, 4T] 是 Tolerating
else sNum++; // [0, T] 是 Satisfied
}
public void calculate() {
value = (int) ((sNum * 10000 + tNum * 10000 / 2) / totalNum);
}
Satisfied 权重 1.0,Tolerating 权重 0.5。结果乘 10000(万分级)避免浮点。DICT 是静态 ConfigurationDictionary,阈值通过 @Arg name 从配置查(:75-76)——不同服务可以有不同 T 阈值。
@MetricsExtension 注解
文件:oap-server/server-core/.../analysis/MetricsExtension.java:32。
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME)
public @interface MetricsExtension {
boolean supportDownSampling(); // 是否支持降采样(小时/天)
boolean supportUpdate(); // 是否支持更新(而非只追加)
boolean timeRelativeID() default false; // ID 是否含时间戳
}
三个字段含义
- supportDownSampling:true 表示该 metrics 能被
toHour()/toDay()降采样。某些指标(如 max)降采样有意义,某些(如 percentile 的中间 dataset)降采样需特殊处��。- supportUpdate:true 表示可被更新(适合"当前值类"指标如 max/min),false 表示纯追加。
- timeRelativeID:true 则实体 ID 带 timeBucket 前缀(如
202211081200-serviceId),典型用于 metadata 级 metrics 如ServiceTraffic。
DataTable 多值指标
DataTable 是什么
DataTable 是一个HashMap<String,Long>的包装,让一个 Metrics 实体在一个时间窗口内同时存多个值,是 percentile(多分位数)、histogram(多 bucket)、labelAvg/labelCount(多标签)的多值载体。
文件:oap-server/server-core/.../analysis/metrics/DataTable.java:41。
public class DataTable implements StorageDataComplexObject<DataTable> {
private HashMap<String, Long> data;
}
DataTable vs 单值 Metrics
| 维度 | 单值 Metrics(CountMetrics/LongAvgMetrics…) | 多值 DataTable(Percentile2/Histogram/Label*) |
|---|---|---|
| 一个时间窗口存 | 1 个最终值(value 列) |
N 个值(一个 HashMap) |
| combine 中间态 | 几个 long 字段(summation/count) | 一个 DataTable(dataset) |
| calculate | 做除法/比较,产出单值 | 遍历 dataset 产出另一个 DataTable |
| 存储列标注 | @Column(dataType=COMMON_VALUE) |
@Column(dataType=LABELED_VALUE/HISTOGRAM, multiIntValues=true) |
| OAL 返回类型 | LongValueHolder/IntValueHolder | LabeledValueHolder/MultiIntValuesHolder |
多值指标怎么存:哈希结构 → 字符串
DataTable 不是直接把 HashMap 存库,而是序列化成字符串。原因在于底层存储(BanyanDB/ElasticSearch)的列式模型不接受 HashMap 类型,只能存标量或字符串。toStorageData()(DataTable.java:134)把整个 HashMap 压成一个字符串:
key1=value1,key2=value2,key3=value3
其中 , 是 Const.ARRAY_SPLIT,= 是 Const.KEY_VALUE_SPLIT(DataTable.java:140,142)。反序列化 toObject()(:148)按 , 切分再按最后一个 = 切分 key/value。
当 key 本身是 DataLabel(如 {p=50})时,key 里也带 =,所以反序列化按最后一个 = 切,而不是第一个。存储形如 {p=50}=120|{p=75}=200|...(| 是 Const.ARRAY_PARSER_SPLIT)。这正是 PercentileMetrics2 注释(:37-39)描述的格式。
这个设计的取舍:用字符串换通用性——任何后端都能存字符串,不必为每种多值结构定制列类型。代价是查询时要做一次解析,但多值指标本来就不是高频过滤字段,可接受。
combine 语义
DataTable 提供多种合并方式(DataTable.java:163-215):
| 方法 | 语义 | 行号 |
|---|---|---|
append(DataTable, maxDataSize) |
同 key 值相加;key 不存在则新增(受 maxDataSize 限制) | :171 |
copyFrom(source) |
等价于 append | :163 |
valueAccumulation(key, value, maxDataSize) |
单 key 累加;超容量则丢弃新 key | :79 |
setMaxValue(DataTable) |
同 key 取较大 | :187 |
setMinValue(DataTable) |
同 key 取较小 | :202 |
sumOfValues() |
所有 value 求和(Percentile 算 total 用) | :95 |
sortedKeys/sortedValues(Comparator) |
按 key 排序取 keys/values(Percentile 排序找分位用) | :106-115 |
典型 combine 流程(以 PercentileMetrics2 为例):
- 每来一条 Source:
dataset.valueAccumulation(value/precision, 1L)——把延迟按精度分桶,桶计数 +1; - 跨实例合并:
this.dataset.append(other.dataset)——两个 DataTable 同桶累加; - calculate:
sumOfValues()得总数 → 按 key 排序 → 累加到每个 rank 的roof = total*p/100时记录该分位值 → 写入percentileValues这个新 DataTable。
maxDataSize 参数用于 LabelCount/LabelAvg 这类"标签可能爆炸"的场景,限制最多保留多少个 label(默认 50,见 @DefaultValue("50"))。没有这个限制,一个高基数标签会把单条 Metrics 的 HashMap 撑爆内存。
在 OAL 里的使用
| OAL 函数 | 对应类 | DataTable 角色 |
|---|---|---|
percentile2 |
PercentileMetrics2 |
dataset(中间桶计数)+ percentileValues(结果 5 个分位值,key={p=50}等) |
percentile(旧) |
PercentileMetrics |
同上但结果用 int[](MultiIntValuesHolder),已废弃 |
histogram |
HistogramMetrics |
dataset(每个 bucket 一项,key=0/100/200...,value=计数),无单值 |
labelCount |
LabelCountMetrics |
dataset(中间按 label 计数)+ value(结果 {n=label},count) |
labelAvg |
LabelAvgMetrics |
summation+count 两个 DataTable + value 结果 DataTable |
DataLabel(metrics/DataLabel.java:28)是 DataTable 的 key 之一:它继承 LinkedHashMap<String,String>,toString() 生成 {k1=v1,k2=v2} 格式(:72-91)。预定义两个 label 名:
GENERAL_LABEL_NAME = "_"(DataLabel.java:29):无标签时的通用 key;PERCENTILE_LABEL_NAME = "p"(:30):分位数标签 key。
buildLabelIndex()(DataTable.java:217)会把 DataTable 的 key 拆成倒排索引(label→keys),供按标签查询使用。
存储列类型标注
DataTable 字段在 @Column 上用特殊 dataType,告诉存储层这是多值结构:
Column.ValueDataType.LABELED_VALUE:带标签的多值(Percentile2、LabelCount、LabelAvg 的 value 列);Column.ValueDataType.HISTOGRAM:直方图(HistogramMetrics 的 dataset,HistogramMetrics.java:45);Column.ValueDataType.COMMON_VALUE:普通单值。
PercentileMetrics 旧版还用 multiIntValues=true(PercentileMetrics.java:59)表示多 int 值,新版改用 LABELED_VALUE + DataTable。
数据流总览(Source → Dispatcher → Metrics → Storage)
把上面三节串起来的完整数据流:

- Receiver 收到上游数据 → 构造某个 Source 子类(如
Service)→ 调SourceReceiver.receive(); Source.prepare()执行(补 ID 字段);SourceDecorator执行(补 attr0~attr5 属性列);SourceDispatcher.dispatch(source)(接口在analysis/SourceDispatcher.java:32)——分发器有两种:硬编码的(在 manual 包内)和 OAL 运行时按模板生成的(注释:24-28)。它把 Source 字段映射成 Metrics 实例并触发combine;- Metrics 进入
MetricsStreamProcessor的 session cache,同 entityId + timeBucket 的会调Metrics.combine(); - 时间窗口结束/降采样时调
Metrics.calculate(),产出最终value或DataTable; - 按
@MetricsExtension(supportDownSampling/supportUpdate)决定是否降采样、是否 update; - 通过
Metrics.id0()生成StorageID(注释Metrics.java:162-167,BanyanDB 用@BanyanDB.SeriesID替代)落库。
关键文件速查
| 子系统 | 关键类 | 路径 |
|---|---|---|
| Source 基类 | ISource / Source | oap-server/server-core/.../source/ISource.java、Source.java |
| Source 全家族 | 56 个子类 | oap-server/server-core/.../source/*.java |
| Scope 注册 | DefaultScopeDefine | oap-server/server-core/.../source/DefaultScopeDefine.java |
| ScopeDeclaration | 同目录 ScopeDeclaration.java |
|
| ScopeDefaultColumn | 同目录 ScopeDefaultColumn.java |
|
| 注解扫描 | AnnotationScan | oap-server/server-core/.../annotation/AnnotationScan.java |
| SourceDecorator | ISourceDecorator / SourceDecoratorManager | oap-server/server-core/.../analysis/ISourceDecorator.java、SourceDecoratorManager.java |
| Metrics 基类 | Metrics | oap-server/server-core/.../analysis/metrics/Metrics.java |
| Metrics 子类 | 18 个函数类 | oap-server/server-core/.../analysis/metrics/*.java |
| MetricsExtension | oap-server/server-core/.../analysis/MetricsExtension.java |
|
| DataTable | DataTable / DataLabel | oap-server/server-core/.../analysis/metrics/DataTable.java、DataLabel.java |
| Dispatcher | SourceDispatcher | oap-server/server-core/.../analysis/SourceDispatcher.java |
未���认事项
- 非
source/目录的 Scope 类(BROWSER_、ZIPKIN_、PROFILE_*、ALARM、EVENT 等)的@ScopeDeclaration类位于server-core其它子包(manual/generic/browser/zipkin/profile 等),注册机制与source/下的类完全相同,本篇未逐一展开其字段。- scopeId 7/14/15/16/77 在
DefaultScopeDefine中无对应常量(被跳过),推测为历史废弃保留,源码无注释说明,未确认具体用途。
接下来
数据模型清楚了,这些数据到底怎么从 Agent 传到 OAP?协议层每条消息长什么样?详见 09-协议层与Agent采集。
心智模型
把数据模型想成工厂的数据字典:Source 是原材料规格书(56 种原料,每种贴一个 Scope 标签,归到 8 个 Catalog 大类),Metrics 是加工配方表(18 种配方,每种 combine 算中间态、calculate 出成品),DataTable 是多格包装盒(一个盒子能同时装 P50/P75/P90/P95/P99 五个值)。OAL 脚本就是工艺单,从某类原材料(Source)取某字段,用某配方(Metrics 函数)加工,产出成品(value 或 DataTable)。