协议层与 Agent 采集
这一篇解决什么
前面的文档讲了 OAP receiver 怎么接收数据,但"线上跑的到底是什么数据结构"没展开。这一篇把协议本身挖到底:apm-protocol全部 26 个 proto 文件、SegmentObject/SpanObject 逐字段、Command 反向下发机制、OAP 端采样策略(基于traceId.hashCode() % 10000的确定性桶)、Compat 兼容协议为什么能让旧 Agent 不断链。读完这篇,Agent ↔ OAP 的"语法"就通了。
apm-protocol 模块结构
apm-protocol 模块只有一个子模块 apm-network,把所有 proto 定义集中托管,通过 protoc 生成各语言桩代码。proto 文件按"数据域"分目录,每个目录对应一类采集能力。
proto 通用约定(新手必读)
所有 proto 共享下面几条约定,看懂这几条就能读懂任何一份:
- package:绝大多数是
skywalking.v3;只有asyncprofiler/AsyncProfiler.proto和pprof/Pprof.proto用skywalking.v10(较新协议,独立版本号)。 - java_package:生成代码落到
org.apache.skywalking.apm.network.<域>.v3,OAP 按 import 区分。 - option java_multiple_files = true:每个 message 生成独立 Java 类,所以你在 OAP 源码里能直接
import ...SegmentObject。 - import “common/Common.proto” / “common/Command.proto”:被引用后,
KeyStringValuePair、CPU、Commands等可直接用。这也解释了为什么"几乎所有 service 的返回值都是Commands"——它们都 import 了Command.proto。 - service 定义:proto 里的
service X { rpc ... }就是 gRPC 服务契约,Agent 是 client,OAP 是 server。 - stream 关键字:
rpc collect (stream SegmentObject)表示客户端流式上传——Agent 在一条 gRPC 连接里连续发多条,returns (Commands)是普通单条响应;双向流(stream ... returns (stream ...))只在 async-profiler/pprof 出现,用来让 OAP 中途控制 Agent 停止。
什么是 protobuf 和 gRPC streaming
- protobuf(Protocol Buffers):Google 的二进制序列化格式。你写一份
.proto,描述数据的字段和类型,protoc 工具帮你生成各语言类。相比 JSON,它更小、更快、有强类型检查,代价是人不方便直接读。- gRPC streaming:普通 RPC 是"一问一答",gRPC 的
stream关键字让一次调用可以连续发多条消息。stream在参数侧叫客户端流(Agent 一直发,OAP 收),在返回值侧叫服务端流,两边都加stream是双向流。SkyWalking 的 trace 上报就是客户端流——一个请求的几十个 span 不一个个发,而是打包成一条流连续推上去。
proto 文件全量清单(26 个)
| 目录 | 文件 | 作用域 |
|---|---|---|
common/ |
Common.proto |
公共基础类型(KeyStringValuePair、CPU、DetectPoint、Instant) |
common/ |
Command.proto |
OAP 反向下发指令的 Command/Commands 结构 |
language-agent/ |
Tracing.proto |
trace 段上报(SegmentObject/SpanObject) |
language-agent/ |
JVMMetric.proto |
JVM 指标上报 |
language-agent/ |
Meter.proto |
自定义 Meter 上报 |
language-agent/ |
CLRMetric.proto |
.NET CLR 指标上报 |
language-agent/ |
ConfigurationDiscoveryService.proto |
配置动态发现(CDS) |
language-agent/ |
TracingCompat.proto / JVMMetricCompat.proto / MeterCompat.proto / CLRMetricCompat.proto |
旧版包名兼容服务定义 |
logging/ |
Logging.proto |
日志上报 |
event/ |
Event.proto |
事件上报 |
profile/ |
Profile.proto |
线程级 Profile 任务下发与快照上报 |
profile/ |
ProfileCompat.proto |
Profile 兼容服务 |
management/ |
Management.proto |
实例注册/保活/属性上报 |
management/ |
ManagementCompat.proto |
管理兼容服务 |
browser/ |
BrowserPerf.proto |
浏览器性能/错误日志/Web Vitals 上报 |
browser/ |
BrowserPerfCompat.proto |
浏览器兼容服务 |
service-mesh-probe/ |
service-mesh.proto |
service mesh(Envoy sidecar)指标上报 |
ebpf/ |
accesslog.proto |
eBPF(Rover)访问日志/网络内核指标 |
ebpf/profiling/ |
Process.proto |
eBPF 进程发现与保活 |
ebpf/profiling/ |
Profile.proto |
eBPF on/off CPU profiling 数据 |
ebpf/profiling/ |
Continuous.proto |
eBPF 连续 profiling 策略 |
asyncprofiler/ |
AsyncProfiler.proto |
async-profiler(JFR)任务(package skywalking.v10) |
pprof/ |
Pprof.proto |
Go pprof 任务(package skywalking.v10) |
各 proto 定义的核心 service 与 message 速览
| proto 文件 | service | 核心 message |
|---|---|---|
language-agent/Tracing.proto:33 |
TraceSegmentReportService(collect 流式 / collectInSync 单条) |
SegmentObject、SpanObject、SegmentReference、Log、SpanAttachedEvent、SegmentCollection |
language-agent/Tracing.proto:235 |
SpanAttachedEventReportService(v3.1,eBPF 附加事件) |
SpanAttachedEvent |
language-agent/JVMMetric.proto:32 |
JVMMetricReportService |
JVMMetricCollection、JVMMetric、Memory、MemoryPool、GC、Thread、Class |
language-agent/Meter.proto:29 |
MeterReportService(collect / collectBatch) |
MeterData、MeterSingleValue、MeterHistogram、MeterBucketValue、Label、MeterDataCollection |
language-agent/CLRMetric.proto:31 |
CLRMetricReportService |
CLRMetricCollection、CLRMetric、ClrGC、ClrThread |
language-agent/ConfigurationDiscoveryService.proto:31 |
ConfigurationDiscoveryService |
ConfigurationSyncRequest |
logging/Logging.proto:32 |
LogReportService |
LogData、LogDataBody、TextLog/JSONLog/YAMLLog、TraceContext、LogTags |
event/Event.proto:30 |
EventService |
Event、Source、Type |
profile/Profile.proto:30 |
ProfileTask |
ProfileTaskCommandQuery、ThreadSnapshot、ThreadStack、ProfileTaskFinishReport、GoProfileData |
management/Management.proto:32 |
ManagementService |
InstanceProperties、InstancePingPkg |
browser/BrowserPerf.proto:31 |
BrowserPerfService |
BrowserPerfData、BrowserErrorLog、BrowserWebVitalsPerfData、BrowserResourcePerfData、BrowserWebInteractionsPerfData |
service-mesh-probe/service-mesh.proto:31 |
ServiceMeshMetricService |
ServiceMeshMetrics、HTTPServiceMeshMetric、TCPServiceMeshMetric、MeshProbeDownstream |
ebpf/accesslog.proto:29 |
EBPFAccessLogService |
EBPFAccessLogMessage、AccessLogConnection、AccessLogKernelLog、AccessLogHTTPProtocol 等 |
ebpf/profiling/Process.proto:31 |
EBPFProcessService |
EBPFProcessReportList、EBPFProcessProperties、EBPFProcessEntityMetadata |
ebpf/profiling/Profile.proto:30 |
EBPFProfilingService |
EBPFProfilingTaskQuery、EBPFProfilingData、EBPFOnCPUProfiling、EBPFOffCPUProfiling |
ebpf/profiling/Continuous.proto:29 |
ContinuousProfilingService |
ContinuousProfilingPolicyQuery、ContinuousProfilingReport、ContinuousProfilingCause |
asyncprofiler/AsyncProfiler.proto:29 |
AsyncProfilerTask |
AsyncProfilerData、AsyncProfilerMetaData、AsyncProfilerTaskCommandQuery、AsyncProfilingStatus |
pprof/Pprof.proto:29 |
PprofTask |
PprofData、PprofMetaData、PprofTaskCommandQuery、PprofProfilingStatus |
common 不定义 service
common/Common.proto只定义公共 message;common/Command.proto只定义Command/Commands,都不定义 service。
SegmentObject / SpanObject 结构详解(trace 核心)
Segment vs Span
Zipkin 这类链路追踪系统,一条 trace 就是一堆平铺的 span。SkyWalking 不一样:它要求"同一个进程(典型如 Java 的同一个线程)里的所有 span 必须先打包成一个 Segment,再整体上报"。这样设计有两个好处:一是减少上报次数——一个请求一个 Segment,而不是几十个 span 各报一次;二是 Segment 本身就是一个分析单位。一条完整的链路通常由多个 Segment 跨进程拼接而成,每个进程报自己的 Segment。
SegmentObject 字段(trace 段对象)
定义见 apm-protocol/apm-network/src/main/proto/language-agent/Tracing.proto:54。
| 字段 | 类型 | 含义 |
|---|---|---|
traceId |
string | 全局 trace 唯一 ID,跨所有 Segment 共享 |
traceSegmentId |
string | 本 Segment 的唯一 ID,其他 Segment 通过它引用本段作为父段 |
spans |
repeated SpanObject | 本段包含的所有 span |
service |
string | 服务逻辑名(一组同行为的工作负载),拓扑图上的节点 |
serviceInstance |
string | 服务实例名(典型是一个 OS 进程,如一个 pod) |
isSizeLimited |
bool | 本 Segment 是否因 span 太多被截断(Agent 可能为了性能丢弃部分 span) |
注释(Tracing.proto:48-53)明确:在 Java 里"一个 Segment = 一个请求上下文在同一个线程里的所有 span";在没有线程概念的语言(如 Go)里,则表示"一个请求上下文跨 goroutine 的所有 span"。
SpanObject 字段(执行单元)
定义见 apm-protocol/apm-network/src/main/proto/language-agent/Tracing.proto:113。
| 字段 | 类型 | 含义 |
|---|---|---|
spanId |
int32 | 本段内 span 编号,从 0 开始,段内唯一 |
parentSpanId |
int32 | 本段内父 span 编号;-1 表示这是根 span(segment 的第一个 span) |
startTime / endTime |
int64 | 毫秒时间戳(1970-01-01 UTC 起算) |
refs |
repeated SegmentReference | 跨线程/跨进程的父段引用;通常只有一个,但 MQ 批量消费场景可能有多个 |
operationName |
string | span 的逻辑名(如 HTTP URI、gRPC 方法签名);强烈建议不含参数,参数放 tags |
peer |
string | 对端地址(ip/host:port);Exit span 必填,用于构建拓扑(STAM 方法) |
spanType |
SpanType | Entry/Exit/Local |
spanLayer |
SpanLayer | 技术栈层级(DB/RPC/HTTP/MQ/Cache 等) |
componentId |
int32 | 组件 ID,对应 component-libraries.yml 里预定义的框架(如 Tomcat=1) |
isError |
bool | 是否异常;决定后端成功率统计 |
tags |
repeated KeyStringValuePair | 字符串键值对,承载参数/额外信息 |
logs |
repeated Log | span 执行期间发生的事件(带时间戳的键值对) |
skipAnalysis |
bool | 为 true 时让后端跳过分析(Agent 比后端更了解服务角色时用) |
关键枚举
SpanType(Tracing.proto:183-190):
| 值 | 含义 | 典型场景 |
|---|---|---|
Entry = 0 |
服务端入口 | RPC 的服务端、MQ 的消费端 |
Exit = 1 |
客户端出口 | RPC 的客户端、MQ 的生产端 |
Local = 2 |
本地代码 | 本地普通代码执行 |
SpanLayer(Tracing.proto:207-224):标识 span 的技术栈层级,用于分层统计:
| 值 | 含义 |
|---|---|
Unknown = 0 |
|
Database = 1 |
数据库客户端 |
RPCFramework = 2 |
RPC 客户端/服务端 |
Http = 3 |
HTTP,是更具体的 RPCFramework |
MQ = 4 |
MQ 生产/消费 |
Cache = 5 |
缓存客户端 |
FAAS = 6 |
函数即服务 |
GenAI = 7 |
生成式 AI 服务,较新 |
SpanType 与 SpanLayer 正交
一个 HTTP 服务端 span 是spanType=Entry, spanLayer=Http;一个 MySQL 查询 span 是spanType=Exit, spanLayer=Database。Entry/Exit 决定拓扑角色,SpanLayer 决定分层统计。
RefType(Tracing.proto:198-204):
| 值 | 含义 |
|---|---|
CrossProcess = 0 |
引用指向另一个 OS 进程的 Segment(跨进程 RPC/MQ) |
CrossThread = 1 |
引用指向同一进程内跨线程的 Segment(仅在有线程概念的语言用) |
SegmentReference 字段(跨段引用)
定义见 Tracing.proto:80。这是把多个 Segment 串成一条链路的"胶水"。
| 字段 | 类型 | 含义 |
|---|---|---|
refType |
RefType | CrossProcess / CrossThread |
traceId |
string | 全局 traceId(与子段相同) |
parentTraceSegmentId |
string | 父 Segment 的 ID |
parentSpanId |
int32 | 父段中具体的父 span 编号 |
parentService |
string | 父段所属服务逻辑名 |
parentServiceInstance |
string | 父段所属实例名 |
parentEndpoint |
string | 父段的 endpoint 名(父段第一个 Entry span 的名) |
networkAddressUsedAtPeer |
string | 父端 Exit span 使用的网络地址(如 127.0.0.1:913),是 STAM 拓扑分析的关键 |
traceId / segmentId / parentSpanId 的关系(链路拼接原理)
这是新手最容易混乱的地方,用一个例子讲清楚:
服务A 收到 HTTP 请求 → 调用 服务B

要点:
traceId全链路唯一,所有 Segment 共享。traceSegmentId段内唯一,是"被引用"的句柄。parentSpanId有两层含义:段内(SpanObject.parentSpanId,指向本段内的父 span)和跨段(SegmentReference.parentSpanId,指向父段里的某个 span)。
tag 与 log 字段
- tag(
SpanObject.tags,Tracing.proto:163):repeated KeyStringValuePair,纯字符串键值对。某些特殊 tag 会被 OAP 用于高级特性(如 Java 插件开发指南里的 special span tags)。通俗说,tag 是 span 的"贴标签",比如db.statement="SELECT * FROM users"、http.method=GET。 - log(
SpanObject.logs,Tracing.proto:166→Log定义于Tracing.proto:174):repeated Log,每个 Log 是{time: int64 毫秒时间戳, data: repeated KeyStringValuePair}。用于记录 span 执行期间的事件,典型用法是发生异常时把异常类名、消息、堆栈作为 key-value 存进去。
SpanAttachedEvent(v3.1,eBPF 附加事件)
定义见 Tracing.proto:246,由 SpanAttachedEventReportService(Tracing.proto:235)流式上报。
这是 Rover 的功能
当一个 RPC 正被进程内语言 Agent trace 时,SkyWalking Rover(eBPF Agent)通过 trace header(sw8)感知到这次 RPC,然后从 OS 内核层面(syscall 级)采集额外的网络诊断信息,"附加"到这个 span 上。
| 字段 | 类型 | 含义 |
|---|---|---|
startTime / endTime |
Instant | 纳秒时间戳(注意:SkyWalking 多数时间戳是毫秒,这里因 syscall 极快而用纳秒) |
event |
string | 事件名(如 syscall 栈里的方法签名) |
tags |
repeated KeyStringValuePair | OS 级信息(如 net_device、L7 协议) |
summary |
repeated KeyIntValuePair | 统计汇总(name → int64) |
traceContext |
SpanReference | 从 sw8 header 解码出的 trace 上下文(traceId/traceSegmentId/spanId) |
Instant(common/Common.proto:58)是纳秒级时间点:seconds(epoch 秒)+ nanos(秒内纳秒,0-999999999)。
SpanReference.SpanReferenceType(Tracing.proto:284)支持 SKYWALKING = 0 和 ZIPKIN = 1,即 Rover 既能附加到 SkyWalking trace,也能附加到 Zipkin trace。
Command 下发机制
为什么几乎所有 rpc 都返回 Commands
SkyWalking 的 gRPC 是双向的。Agent 上报数据的同时,OAP 借助每个 rpc 的返回值Commands反向给 Agent 下发指令——采样率调整、Profile 任务、配置更新、eBPF profiling 任务等。Agent 不需要额外的轮询通道。复用 Agent 已经在建立的 gRPC 连接做反向控��。Agent 每次上报(trace/JVM/meter/log…),OAP 都可以顺带把"接下来你要做什么"塞进响应里,免去 Agent 单独长轮询配置/任务的开销。这也是为什么这些 proto 全都
import "common/Command.proto"。
什么是 Command 下发
“Command 下发"就是 OAP 在 Agent 上报数据的回程里,夹带一条指令给 Agent 执��。比如 OAP 想 profile 某个慢接口,不用等 Agent 来问,而是趁 Agent 下一次上报 trace 时,在返回值里塞一句"去采集这个 endpoint 的线程快照”。Agent 收到后照做,下次上报时再把结果带回来。本质是把"控制流"搭在"数据流"的回程上,省一条独立通道。
Command / Commands 结构
定义见 apm-protocol/apm-network/src/main/proto/common/Command.proto。
Command(Command.proto:99):
| 字段 | 类型 | 含义 |
|---|---|---|
command |
string | 指令名,区分不同指令类型(如 ProfileTaskQuery、ConfigurationDiscoveryCommand) |
args |
repeated KeyStringValuePair | 指令参数,键值对;基本类型转字符串、列表用逗号分隔、复杂结构用 JSON 序列化 |
Commands(Command.proto:112):repeated Command commands,一次可下发多条指令。
通用信封设计
Command 是"无 schema 的通用信封"——command名字 +args键值对。具体每条指令有哪些 arg,由注释文档约定(见Command.proto:32-98),不强制在 proto 里枚举。新增指令类型不用改 proto,只需新增一个指令名和约定 args。
OAP 侧的 Command 类型实现
OAP 在 apm-protocol/apm-network/src/main/java/org/apache/skywalking/oap/server/network/trace/component/command/ 下为每种指令写了 Java 封装类,统一继承 BaseCommand,实现 Serializable(OAP→Agent 方向序列化)和 Deserializable(Agent→OAP 方向反序列化,用于去重/确认)。
BaseCommand(BaseCommand.java:24):所有指令都带 command 名和 SerialNumber(序列号,Agent 用于指令去重,避免重复执行同一指令)。它内部维护一个 Command.Builder,构造时先把 SerialNumber 塞进去(BaseCommand.java:36)。
已实现的指令类型:
| Java 类 | 指令名(NAME 常量) | 说明 |
|---|---|---|
ProfileTaskCommand.java:28 |
ProfileTaskQuery |
给 Agent 下发线程级 Profile 任务(taskId、endpoint、duration、dumpPeriod 等) |
ConfigurationDiscoveryCommand.java:29 |
ConfigurationDiscoveryCommand |
CDS 配置发现,含 UUID(配置指纹)和 config 键值对 |
EBPFProfilingTaskCommand.java |
EBPFProfilingTaskQuery |
给 Rover 下发 eBPF profiling 任务 |
ContinuousProfilingPolicyCommand.java |
ContinuousProfilingPolicyQuery |
下发连续 profiling 策略(阈值监控) |
ContinuousProfilingReportCommand.java |
ContinuousProfilingReportTask |
处理 Agent 上报的连续 profiling 触发任务 |
AsyncProfilerTaskCommand.java |
(async-profiler 任务) | 给 Java Agent 下发 async-profiler(JFR)任务 |
PprofTaskCommand.java |
(pprof 任务) | 给 Go Agent 下发 pprof 任务 |
TraceIgnoreCommand.java |
(trace 忽略规则) | 下发 trace 忽略规则 |
ProfileTaskCommand 详解(最典型的指令)
ProfileTaskCommand.java:26 实现的指令 ProfileTaskQuery(对应 Command.proto:43-53 的约定)字段:
TaskId:任务唯一 IDEndpointName:要 profile 的 endpoint 名Duration:profile 持续时长(秒)MinDurationThreshold:最小耗时门槛,只有慢于这个值的请求才 profileDumpPeriod:线程快照 dump 间隔(毫秒)MaxSamplingCount:最大采样快照数StartTime/CreateTime:任务起始/创建时间戳
序列化时(ProfileTaskCommand.java:92)把这些字段逐个转成字符串塞进 args;反序列化时(ProfileTaskCommand.java:54)遍历 args 按 key 还原。这正是"通用信封 + 约定 args"的落地方式。
配置发现的 UUID 优化
ConfigurationDiscoveryService.proto:40 的 fetchConfigurations rpc 返回 Commands,期望指令是 ConfigurationDiscoveryCommand。注释(ConfigurationDiscoveryService.proto:33-39)说明:用一个保留 key UUID 作为配置指纹——Agent 缓存上次的 UUID,下次请求带上它(ConfigurationSyncRequest.uuid,ConfigurationDiscoveryService.proto:53),如果配置没变 OAP 返回空 Commands,减少流量。ConfigurationDiscoveryCommand.java:31 也定义了 UUID_CONST_NAME = "UUID"。
采样协议与策略
采样在 OAP 后端做,不在 proto 里
SkyWalking 的 trace 采样主要在 OAP 后端完成,而不是 Agent 端全量决定。Agent 默认全量上报,OAP 用一个基于traceId.hashCode()的 0-9999 概率桶 + 慢/错误 trace 优先的策略,决定哪些 trace 真正落库。采样率可以按 service 维度配置,且支持动态下发。采样策略本身不在 proto 协议里显式定义字段。
SegmentObject没有专门的"采样率"字段。采样是 OAP 收到 Segment 后的"丢弃/保留"决策,不改变 Agent 上报的格式。
什么是采样限流
线上流量一大,每秒几十万个 trace 全存下来,存储扛不住、查询也慢。“采样"就是只保留一部分 trace 落库——比如只留 30%,剩下 70% 收到即丢。关键是怎么决定"留哪些”:随机丢可能正好把出问题的慢请求丢了,所以 SkyWalking 的策略是"确定性桶 + 慢 trace 优先"——同一个 traceId 永远得到同一个采样号(要么全留要么全丢,保证链路完整),而且只要这个请求够慢(超过阈值),不管采样号是多少都留。
协议层与采样的唯一联系
SpanObject.isError和startTime/endTime(算 duration)是 OAP 采样决策的输入。- 采样率配置不是通过 Command 下发给 Agent,而是 OAP 侧自己加载
trace-sampling-policy-settings.yml(静态文件 + 动态配置),由traceSamplingPolicy这个 dynamic config key 驱动。
新手常见误解
“采样率是 OAP 通过 Command 下发给 Agent 的”。当前实现里不是——采样是 OAP 端的后置过滤。Command 下发的是 Profile 任务、配置发现等,不是 trace 采样率。
OAP 侧采样实现
路径修正
原文把TraceSegmentSampler和TraceSamplingPolicyWatcher都归到trace/sampling/目录,实际不是。TraceSegmentSampler.java在trace/parser/listener/,TraceSamplingPolicyWatcher.java在trace/(sampling 子目录只放SamplingPolicy、SamplingPolicySettings、SamplingPolicySettingsReader三个纯数据类)。
核心类分散在两个目录:
TraceSegmentSampler(oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/TraceSegmentSampler.java:30):
// TraceSegmentSampler.java:33
public boolean shouldSample(SegmentObject segmentObject, int duration) {
int sample = Math.abs(segmentObject.getTraceId().hashCode()) % 10000;
String serviceName = segmentObject.getService();
return traceSamplingPolicyWatcher.shouldSample(serviceName, sample, duration);
}
逻辑很简单:用 traceId.hashCode() 取模 10000 得到 sample(0-9999 的确定性桶),再交给 TraceSamplingPolicyWatcher.shouldSample(service, sample, duration) 判定。关键是同一条 trace 的所有 Segment 因为 traceId 相同,hash 结果相同,要么全采要么全丢——保证了链路完整性。如果用随机数,同一个请求的 A 段采了、B 段丢了,链路就断了。
SamplingPolicy(oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/sampling/SamplingPolicy.java:31):极简值对象,两个字段 rate(采样率,0-10000)和 duration(慢 trace 阈值,毫秒)。
SamplingPolicySettings(SamplingPolicySettings.java:30):
defaultPolicy:全局默认策略,构造时为new SamplingPolicy(10000, -1),即 10000/10000=100% 采样,慢阈值 -1 表示不启用慢 trace 优先(SamplingPolicySettings.java:39)。services:Map<String, SamplingPolicy>(SamplingPolicySettings.java:31),按 service 名覆盖默认策略。
采样精度
采样率精度是 1/10000,10000 表示 100%;慢 trace 阈值默认 -1(不采样慢 trace);单位毫秒。
TraceSamplingPolicyWatcher(oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/TraceSamplingPolicyWatcher.java:37):继承 ConfigChangeWatcher,key 为 traceSamplingPolicy(:44)。判定逻辑(:73-113):

- 取该 service 的
SamplingPolicy;没有则用默认(TraceSamplingPolicyWatcher.java:73-79)。 - 优先级:慢阈值 > 采样率(注释
:98-100)。意思是只要 duration 超了慢阈值就采,不管采样号。 shouldSampleByDefault(sample, duration)(:89):duration >= 默认慢阈值或sample < 默认采样率则采样。shouldSampleService(...)(:107-113):服务级慢阈值或服务级采样率命中则采样;服务级某项为 null 则回退到全局对应项。
withinRateRange 的实现就是 currentSample < policySample(:127-129)——sample 是 0-9999 的桶号,只要桶号小于采样率阈值就采。所以 rate=3000 意味着桶号 0-2999 采(30%),3000-9999 丢。
全量 vs 慢/错误 trace 的区别
| 模式 | 配置 | 行为 |
|---|---|---|
| 全量(默认) | rate=10000 |
所有 trace 都采(sample < 10000 恒真) |
| 采样模式 | 如 rate=3000 |
30% 概率采(sample < 3000 才采),其余丢弃省存储 |
| 慢 trace 优先 | duration=500(ms) |
无论是否命中采样率,只要耗时 ≥500ms 就采——保证慢请求的链路不丢 |
错误 trace
SpanObject.isError=true不会被采样器直接豁免(采样器只看 duration 和 sample),但 OAP 在isSizeLimited等场景对错误 span 有保留逻辑。未确认错误 trace 是否有独立采样豁免分支。
配置来源与动态更新
- 静态文件:
trace-sampling-policy-settings.yml,由SamplingPolicySettingsReader(SamplingPolicySettingsReader.java:35)用 SnakeYAML 解析。结构(:61-86):顶层default: {rate, duration}+services: [{name, rate, duration}, ...]。 - 动态更新:
TraceSamplingPolicyWatcher注册为traceSamplingPolicy配置的 watcher(:44),当动态配置(如 ZooKeeper/Apollo/配置文件热更)下发新 YAML 字符串时,notify()(:50-58) →activeSetting()(:136-141) →parseFromYml()(:164-175)重新解析并替换samplingPolicySettings(:143-151)。解析失败则保留上一份配置,不会回退到默认。
Compat 协议
Compat 是什么
*Compat.proto是"旧版 proto 包名"的兼容服务定义,让使用旧版skywalking.v3包名(无独立 package 声明、java_package 不同)的 Agent 仍能连上新版 OAP。OAP 侧用*HandlerCompat类把这些旧包名请求委托给新版 handler 处理,从而实现 proto 字段演进时旧 Agent 不断链。
各 Compat 文件的作用
| Compat proto | 对应主 proto | java_package |
|---|---|---|
language-agent/TracingCompat.proto |
Tracing.proto | ...language.agent.v3.compat |
language-agent/JVMMetricCompat.proto |
JVMMetric.proto | ...language.agent.v3.compat |
language-agent/MeterCompat.proto |
Meter.proto | ...language.agent.v3.compat |
language-agent/CLRMetricCompat.proto |
CLRMetric.proto | ...language.agent.v3.compat |
management/ManagementCompat.proto |
Management.proto | ...management.v3.compat |
profile/ProfileCompat.proto |
Profile.proto | ...language.profile.v3.compat |
browser/BrowserPerfCompat.proto |
BrowserPerf.proto | ...language.agent.v3.compat |
为什么需要 Compat
gRPC 服务发现基于全限定方法名
proto 的字段编号一旦发布就不能改(改了就破坏线上的 Agent)。但有时需要"演进服务定义"——比如调整 java_package、调整 service 方法集合。问题在于:gRPC 的服务发现基于全限定方法名(包含 package)。如果直接改主 proto 的package,旧版 Agent(已编译进老包名)发出的请求方法名对不上新版 OAP 注册的方法名,直接断链。Compat 的解法:保留一份"旧包名、旧 service 名"的 proto(
option deprecated = true标记废弃),让 OAP 同时注册新旧两套 gRPC 服务。旧 Agent 连旧服务名、新 Agent 连新服务名,OAP 内部都用同一套 handler 处理。
Compat proto 的写法特征
观察 TracingCompat.proto:
- 没有
package skywalking.v3;声明(默认包,模拟旧版无 package 的状态)。 option java_package = "...language.agent.v3.compat"(TracingCompat.proto:22),生成的 gRPC stub 类落到compat子包。option deprecated = true(TracingCompat.proto:25):标记这是废弃的兼容层。import "language-agent/Tracing.proto"(TracingCompat.proto:28):复用新版 message 定义(skywalking.v3.SegmentObject等),只是 service 用旧包名包一层。- service 体里直接引用
skywalking.v3.SegmentObject/skywalking.v3.Commands(见TracingCompat.proto:36-43),即 message 是共享的,只是 service 全限定名不同。
*HandlerCompat 如何把旧请求转新处理
每个 receiver 插件都有一个 *HandlerCompat 类,它继承旧包名生成的 gRPC 基类,但内部直接委托给新版 handler。以 TraceSegmentReportServiceHandlerCompat(oap-server/server-receiver-plugin/skywalking-trace-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/trace/provider/handler/v8/grpc/TraceSegmentReportServiceHandlerCompat.java:30)为例:
// TraceSegmentReportServiceHandlerCompat.java:30
public class TraceSegmentReportServiceHandlerCompat
extends TraceSegmentReportServiceGrpc.TraceSegmentReportServiceImplBase // 旧包名 stub
implements GRPCHandler {
private final TraceSegmentReportServiceHandler delegate; // 新版 handler
public StreamObserver<SegmentObject> collect(StreamObserver<Commands> responseObserver) {
return delegate.collect(responseObserver); // 直接转发
}
public void collectInSync(SegmentCollection request, StreamObserver<Commands> responseObserver) {
delegate.collectInSync(request, responseObserver);
}
}
因为 Compat proto 复用了 skywalking.v3.SegmentObject(同一个生成类),新旧 handler 收到的 SegmentObject 是同一个 Java 类型,所以零成本转发——不需要任何字段转换。
Compat 不是协议转换器
Compat 不是"旧字段转新字段"的协议转换器,而是"旧 gRPC 服务名 → 新 handler"的桥接器。真正的字段演进兼容靠 proto3 的"新增字段不破坏旧 consumer"特性保证,Compat 只解决 service 全限定名变化的问题。
JVM/CLR/Meter/Log/Event 的 message 结构
JVMMetricCollection / JVMMetric(JVM 指标)
定义见 apm-protocol/apm-network/src/main/proto/language-agent/JVMMetric.proto。service 是 JVMMetricReportService.collect(JVMMetricCollection) returns (Commands)(:32)。
JVMMetricCollection(:37):repeated JVMMetric metrics + service + serviceInstance。一次上报一个实例的多个时间点指标。
JVMMetric(:43)字段:
| 字段 | 类型 | 含义 |
|---|---|---|
time |
int64 | 指标时间戳(毫秒) |
cpu |
CPU | CPU 使用率(CPU.usagePercent: double,见 Common.proto:40) |
memory |
repeated Memory | 内存(堆/非堆) |
memoryPool |
repeated MemoryPool | 内存池 |
gc |
repeated GC | GC 统计 |
thread |
Thread | 线程统计 |
clazz |
Class | 类加载统计 |
Memory(:53):isHeap(是否堆)、init、max、used、committed(int64,字节)。
MemoryPool(:61):type(PoolType 枚举)+ init/max/used/committed。PoolType(:69)枚举了 CODE_CACHE/NEWGEN/OLDGEN/SURVIVOR/PERMGEN/METASPACE/ZHEAP 等 11 种 JVM 内存池类型。
GC(:83):phase(GCPhase:NEW/OLD/NORMAL,NORMAL 用于 ZGC 这类无新旧代之分的 GC)+ count + time。
Thread(:96):liveCount、daemonCount、peakCount,以及各状态线程数(runnableStateThreadCount、blockedStateThreadCount、waitingStateThreadCount、timedWaitingStateThreadCount)。
Class(:107):loadedClassCount、totalUnloadedClassCount、totalLoadedClassCount。
CLRMetric(.NET CLR 指标)
定义见 CLRMetric.proto,结构对称于 JVM。
CLRMetricCollection(:36)与 CLRMetric(:42):time + cpu + gc(ClrGC) + thread(ClrThread)。
ClrGC(:49):Gen0/Gen1/Gen2 回收次数 + HeapMemory。
ClrThread(:56):可用/最大 CompletionPort 线程数、可用/最大 Worker 线程数。
MeterData(自定义 Meter)
定义见 Meter.proto。service MeterReportService 有两个 rpc:
collect(stream MeterData)(:32):每个 stream 作为 MAL 引擎的一个完整输入数据集,client 应每周期onComplete一次流。collectBatch(stream MeterDataCollection)(:38):批量模式,流里每个MeterDataCollection都被当作 MAL 的一个完整输入。
MeterData(:80)字段:
| 字段 | 类型 | 含义 |
|---|---|---|
metric |
oneof | singleValue(MeterSingleValue)或 histogram(MeterHistogram) |
service |
string | 服务名(流首元素设置) |
serviceInstance |
string | 实例名(流首元素设置) |
timestamp |
int64 | 上报时间(流首元素设置) |
Label(:43):name + value,metric 的标签维度。
MeterSingleValue(:60):name + repeated Label labels + value: double,单一数值型指标。
MeterHistogram(:70):name + labels + repeated MeterBucketValue values,直方图。
MeterBucketValue(:49):bucket(桶下界,double)+ count(桶内计数,int64)+ isNegativeInfinity(是否负无穷,为 true 时 bucket 值无效)。桶上界由"下一个 MeterBucketValue 的 bucket"决定,最后一个桶上界为正无穷。
当前 Meter 只有两种类型
当前版本的Meter.proto中MeterData.metric的 oneof(Meter.proto:82-84)只有singleValue和histogram两种,无mMultiIntValue字段。MultiIntValuesHolder是 OAPserver-core里 metrics 存储层的概念(用于多 int 值 metrics 如 percentile),与 Meter 上报协议无关。
MeterDataCollection(:94):repeated MeterData meterData,用于 collectBatch。
LogData(日志)
定义见 logging/Logging.proto。service LogReportService.collect(stream LogData) returns (Commands)(:32)。
LogData(:42)字段:
| 字段 | 类型 | 含义 |
|---|---|---|
timestamp |
int64 | 日志时间(毫秒,可选;不设则 OAP 用接收时间) |
service |
string | 服务名(必填;流式上报中非首元素可复用前一个非空值) |
serviceInstance |
string | 实例名(可选) |
endpoint |
string | endpoint(可选) |
body |
LogDataBody | 日志内容(必填) |
traceContext |
TraceContext | 关联的 trace 上下文(可选) |
tags |
LogTags | 标签(可选,供 OAP 搜索/分析) |
layer |
string | 服务/实例所属 layer(9.0.0+,缺省则 OAP 设为 general) |
LogDataBody(:75):type(字符串,匹配 OAP 侧 analyzer)+ oneof content:TextLog.text / JSONLog.json / YAMLLog.yaml。
TraceContext(:103):traceId + traceSegmentId + spanId——当 Agent 把 trace ID 注入日志文本时,用这个把日志和链路关联起来。
LogTags(:113):repeated KeyStringValuePair data。
Event(事件)
定义见 event/Event.proto。service EventService.collect(stream Event) returns (Commands)(:30)。注释(:31-34):一个事件通常调两次 collect——一次开始一次结束,用同一个 UUID 关联;若起止时间都已知(如从第三方系统导入)则只调一次。
Event(:38)字段:
| 字段 | 类型 | 含义 |
|---|---|---|
uuid |
string | 事件唯一 ID,关联开始/结束 |
source |
Source | 事件发生源 |
name |
string | 事件名(如 Reboot、Upgrade) |
type |
Type | Normal=0 / Error=1,影响 UI 颜色 |
message |
string | 一行简述(不建议塞详细日志/堆栈) |
parameters |
map<string,string> | message 里的参数 |
startTime |
int64 | 起始时间(毫秒,必填) |
endTime |
int64 | 结束时间(毫秒,可空表示未结束) |
layer |
string | 所属 layer(9.0.0+ 必填) |
Source(:82):service + serviceInstance + endpoint,按粒度选择性必填(注释 :79-81:仅 service 事件只需 service;instance 事件需 service+serviceInstance;endpoint 事件需 service+endpoint)。
其他域 message 速览
- Management(
Management.proto):InstanceProperties(service+serviceInstance+properties+layer,上报实例自定义属性)、InstancePingPkg(service+serviceInstance+layer,保活心跳)。 - BrowserPerf(
BrowserPerf.proto):BrowserPerfData(页面级性能:redirectTime/dnsTime/ttfbTime/tcpTime/transTime/domAnalysisTime/fptTime/domReadyTime/loadPageTime/resTime/sslTime/ttlTime/firstPackTime/fmpTime 等毫秒级字段);BrowserErrorLog(错误日志,含 category 枚举 ajax/resource/vue/promise/js/unknown、grade、message、line、col、stack、errorUrl、firstReportedError);BrowserWebVitalsPerfData(FMP、CLS、LCP);BrowserResourcePerfData(资源加载);BrowserWebInteractionsPerfData(INP)。浏览器场景里serviceVersion对应后端的 Instance 概念、pagePath对应 Endpoint 概念(注释:56,61)。 - Service Mesh(
service-mesh.proto):ServiceMeshMetricsoneofhttpMetrics/tcpMetrics;HTTPServiceMeshMetric含 source/dest 服务名+实例、endpoint、latency、responseCode、status、protocol、detectPoint、tlsMode、internalErrorCode、Envoy 内部请求/响应延迟(纳秒);TCPServiceMeshMetric额外有 receivedBytes/sentBytes。返回值是空MeshProbeDownstream(:133)而非 Commands,是少数例外。 - eBPF accesslog(
ebpf/accesslog.proto):EBPFAccessLogMessage(node 信息 + connection + kernelLogs + protocolLog),含 L2/L3/L4 网络栈纳秒级延迟指标、HTTP 协议解析、trace 信息;返回空EBPFAccessLogDownstream。 - eBPF profiling(
ebpf/profiling/):EBPFProfilingDataoneofonCPU/offCPU(Profile.proto:48),栈元数据区分PROCESS_KERNEL_SPACE/PROCESS_USER_SPACE;ContinuousProfilingReport(Continuous.proto:53)含触发原因ContinuousProfilingCause(CPU/线程数/系统负载/HTTP 错误率/HTTP 平均响应时间阈值),目标 task oneof onCPU/offCPU/network。 - eBPF process(
ebpf/profiling/Process.proto):EBPFProcessPropertiesoneof hostProcess/k8sProcess,含EBPFProcessEntityMetadata(layer+serviceName+instanceName+processName+labels)。 - async-profiler(
AsyncProfiler.proto,packageskywalking.v10):AsyncProfilerData(metaData + oneof errorMessage/content(JFR 二进制));AsyncProfilingStatus枚举 PROFILING_SUCCESS/EXECUTION_TASK_ERROR/TERMINATED_BY_OVERSIZE;collect是双向流,OAP 可中途用响应控制停止(如文件超限)。 - pprof(
Pprof.proto,packageskywalking.v10):结构与 async-profiler 对称,PprofData(metadata + errorMessage/content pprof 二进制),同样双向流。
附录:componentId 与 component-libraries.yml
SpanObject.componentId(Tracing.proto:153)是一个预定义数字 ID,代表本次 span 用的框架/技术栈。所有 ID 在 oap-server/server-starter/src/main/resources/component-libraries.yml 集中定义(component-libraries.yml:43 起,Unknown: id=0),如 Tomcat: id=1、HttpClient: id=2、Dubbo: id=3、H2: id=4、Mysql: id=5 等。
ID 一旦发布不可更改
注释(component-libraries.yml:17)强调:ID 一旦发布不可更改,只能追加且必须唯一,废弃的 ID 永久保留,否则会导致可视化和聚合错误。每个���件还声明languages(哪些语言用)和priority(0-100,越接近业务代码优先级越高,默认 50,用于决定同一 span 多组件叠加时哪个胜出)。Agent 上报时只传数字 ID,OAP 据此反查组件名,省去传输字符串开销。
关键结论汇总
- apm-protocol 是纯协议层:只定义 proto + 少量 Command 序列化工��类,不含任何接收/分析逻辑;接收在 receiver 插件,分析在 analyzer 模块。
- trace 模型是 Segment 优先:不是"一堆 span 平铺",而是"每进程一个 Segment 包含若干 span",跨进程靠
SegmentReference拼接,这是 SkyWalking 区别于 Zipkin 的核心设计。 - Command 是反向控制的通用信封:
command名 +args键值对,复用所有 rpc 的returns (Commands)通道,新增指令不改 proto。 - 采样在 OAP 后端做,不在 proto 里:基于
traceId.hashCode() % 10000的确定性桶 + 慢 trace 优先,按 service 配置,走 OAP 动态配置体系而非 gRPC Command 下发。 - Compat 解决的是 gRPC 服务全限定名变化,不是字段转换:靠"旧包名 service + 共享 message + HandlerCompat 委托新版 handler"实现旧 Agent 不断链。
- 当前 Meter 只有 singleValue 和 histogram(
Meter.proto:82-84),无mMultiIntValue字段。
关键文件速查
| 子系统 | 关键文件 | 路径 |
|---|---|---|
| trace 协议 | Tracing.proto | apm-protocol/apm-network/src/main/proto/language-agent/Tracing.proto |
| Command | Command.proto | apm-protocol/apm-network/src/main/proto/common/Command.proto |
| 公共类型 | Common.proto | apm-protocol/apm-network/src/main/proto/common/Common.proto |
| JVM | JVMMetric.proto | apm-protocol/apm-network/src/main/proto/language-agent/JVMMetric.proto |
| Meter | Meter.proto | apm-protocol/apm-network/src/main/proto/language-agent/Meter.proto |
| Log | Logging.proto | apm-protocol/apm-network/src/main/proto/logging/Logging.proto |
| Event | Event.proto | apm-protocol/apm-network/src/main/proto/event/Event.proto |
| eBPF | accesslog.proto + profiling/*.proto | apm-protocol/apm-network/src/main/proto/ebpf/ |
| async-profiler | AsyncProfiler.proto | apm-protocol/apm-network/src/main/proto/asyncprofiler/AsyncProfiler.proto |
| pprof | Pprof.proto | apm-protocol/apm-network/src/main/proto/pprof/Pprof.proto |
| Compat | 各 *Compat.proto | apm-protocol/apm-network/src/main/proto/{language-agent,management,profile,browser}/*Compat.proto |
| Command Java 封装 | BaseCommand + 各 *Command | apm-protocol/apm-network/src/main/java/.../network/trace/component/command/ |
| 采样入口 | TraceSegmentSampler | oap-server/analyzer/agent-analyzer/.../provider/trace/parser/listener/TraceSegmentSampler.java |
| 采样策略 watcher | TraceSamplingPolicyWatcher | oap-server/analyzer/agent-analyzer/.../provider/trace/TraceSamplingPolicyWatcher.java |
| 采样策略数据类 | SamplingPolicy / SamplingPolicySettings / SamplingPolicySettingsReader | oap-server/analyzer/agent-analyzer/.../provider/trace/sampling/ |
| componentId | component-libraries.yml | oap-server/server-starter/src/main/resources/component-libraries.yml |
接下来
协议清楚了,OAP 怎么对运行中进程做更深的性能剖析?四种 profiling 体系怎么对比?详见 10-Profiling体系。
心智模型
把 apm-protocol 想成"Agent 和 OAP 之间的合同":proto 文件是合同条款(每个数据域一份),Segment 是"每进程打包上报的集装箱"(里面装 span),Command 是"OAP 塞在回程包裹里的指令条"(复用上报通道反向下发),采样是"OAP 收货后决定哪些集装箱入库的筛选规则"(基于 traceId 的确定性桶,慢件优先),Compat 是"旧版合同的复印件"(让还在用旧合同的 Agent 也能签收,OAP 内部统一处理)。