【SkyWalking从入门到精通】第49篇:SkyWalking存储模型全景图:四大模型如何撑起海量观测数据
上一篇【第48篇】OAP集群规划实战:从理论到生产的容量设计
下一篇【第50篇】注册模型与明细模型:名字的管家和现场的记录官
一、开篇:不是所有的数据都叫"观测数据"
APM系统要存储的数据五花八门——服务名、Trace ID、响应时间、HTTP状态码、JVM指标、调用拓扑……如果像NoSQL那样用一个万能JSON把什么都往里塞,查询性能会惨不忍睹。
SkyWalking的设计者做了一个聪明的分类:把所有数据按"使用方式"分成四大类型,每种类型用专门的存储结构和优化策略。这就是四大存储模型的由来。
APM数据
=======
|
| 按"怎么用"来分类
v
+---+---+---+---+
| | | | |
v v v v v
注册 明细 指标 采样
模型 模型 模型 模型
查得少 查得多 查得多 查得少
写得少 写得多 读得频繁 按需获取
小体积 大体积 小体积 大体积
永久 有限TTL 有限TTL 有限TTL
图1:四大模型对APM数据的分类策略
二、四种模型的特征速览
| 维度 | 注册模型 | 明细模型 | 指标模型 | 采样模型 |
|---|---|---|---|---|
| 存储内容 | 服务名、端点名等实体 | Trace/Span明细 | 聚合后的数值指标 | 慢查询/异常堆栈 |
| 数据量 | 很小(千级) | 极大(亿级/天) | 中等(万级/天) | 小(按规则触发) |
| 写频率 | 极低(几乎不变) | 极高(实时) | 中(分钟级) | 低(按需) |
| 读频率 | 高(Dashboard常查) | 低(问题排查时) | 极高(Dashboard) | 低(问题排查时) |
| 保留周期 | 永久 | 7-14天 | 分钟:30天/小时:90天/天:180天 | 7-14天 |
| 典型大小 | KB级 | GB-TB级 | MB-GB级 | MB级 |
三、注册模型(Register Model)
3.1 解决什么问题?
当探针上报一个Span说"这是user-service调用了order-service的/createOrder接口",OAP需要把这些名字存起来。但每次Span都带完整名字太浪费,于是SkyWalking做了一层名字到ID的映射:
name = "user-service" --> serviceId = 1
name = "order-service" --> serviceId = 2
name = "/createOrder" --> endpointId = 101
3.2 注册模型的类结构
// 注册模型 - 服务注册
@Stream(name = "service_inventory", scopeId = 0)
public class ServiceInventory extends RegisterSource {
@Column(columnName = "service_id")
@IDColumn
private int serviceId; // 服务ID(主键)
@Column(columnName = "name")
@NeedDefault
private String name; // 服务名称
@Column(columnName = "is_address")
private int isAddress; // 是否是地址
@Column(columnName = "properties")
private String properties; // 自定义属性
}
// 注册模型 - 端点注册
@Stream(name = "endpoint_inventory", scopeId = 0)
public class EndpointInventory extends RegisterSource {
@Column(columnName = "endpoint_id")
@IDColumn
private int endpointId; // 端点ID
@Column(columnName = "service_id")
private int serviceId; // 所属服务ID
@Column(columnName = "name")
private String name; // 端点名称
@Column(columnName = "detect_point")
private int detectPoint; // 探测点类型
}
// 注册模型 - 网络地址注册
@Stream(name = "network_address_inventory", scopeId = 0)
public class NetworkAddressInventory extends RegisterSource {
@Column(columnName = "address_id")
@IDColumn
private int addressId;
@Column(columnName = "name")
private String name;
}
四、明细模型(Record Model)
4.1 解决什么问题?
明细模型存储的是"原始证据"——每一条Trace、每一个Span的完整信息。当需要排查问题时,这些"现场的细节"是必不可少的。
4.2 明细模型示例
// 明细模型 - Segment记录(一个Trace的完整记录)
@Stream(name = "segment", scopeId = 1)
public class SegmentRecord extends Record {
@Column(columnName = "segment_id")
@IDColumn
private String segmentId; // Segment唯一ID
@Column(columnName = "trace_id")
private String traceId; // 所属Trace ID
@Column(columnName = "service_id")
private int serviceId; // 服务ID
@Column(columnName = "service_instance_id")
private int serviceInstanceId; // 实例ID
@Column(columnName = "endpoint_id")
private int endpointId; // 端点ID
@Column(columnName = "start_time")
private long startTime; // 开始时间
@Column(columnName = "end_time")
private long endTime; // 结束时间
@Column(columnName = "latency")
private int latency; // 延迟(ms)
@Column(columnName = "is_error")
private int isError; // 是否出错
@Column(columnName = "data_binary")
private byte[] dataBinary; // Span列表的二进制数据
@Column(columnName = "version")
private int version; // 协议版本
}
// 明细模型 - 告警记录
@Stream(name = "alarm_record", scopeId = 1)
public class AlarmRecord extends Record {
@Column(columnName = "alarm_record_id")
@IDColumn
private String id;
@Column(columnName = "rule_name")
private String ruleName;
@Column(columnName = "scope")
private int scope;
@Column(columnName = "scope_id")
private int scopeId;
@Column(columnName = "name")
private String name;
@Column(columnName = "alarm_message")
private String alarmMessage;
@Column(columnName = "start_time")
private long startTime;
@Column(columnName = "value")
private String value;
}
五、指标模型(Metrics Model)
指标模型是SkyWalking当之无愧的"主角",因为Dashboard上的每个图表背后都有指标模型在支撑。
// 指标模型 - 服务响应时间指标
@Stream(name = "service_resp_time", scopeId = 2)
@MetricsExtension(supportDownSampling = true)
public class ServiceRespTimeMetrics extends Metrics
implements LongValueHolder {
@Column(columnName = "entity_id")
@IDColumn
private String entityId;
@Column(columnName = "value",
dataType = Column.ValueDataType.COMMON_VALUE,
function = Function.Avg)
private long value; // 平均值
@Column(columnName = "total")
private long total; // 调用次数
@Column(columnName = "sum")
private long sum; // 延迟总和
@Column(columnName = "time_bucket")
private long timeBucket; // 时间窗口
}
// 指标模型 - 百分位响应时间指标
@Stream(name = "service_percentile", scopeId = 2)
public class ServicePercentileMetrics extends Metrics
implements MultiLongValuesHolder {
@Column(columnName = "entity_id")
@IDColumn
private String entityId;
@Column(columnName = "p50")
private long p50;
@Column(columnName = "p75")
private long p75;
@Column(columnName = "p90")
private long p90;
@Column(columnName = "p95")
private long p95;
@Column(columnName = "p99")
private long p99;
@Column(columnName = "total")
private long total;
@Column(columnName = "time_bucket")
private long timeBucket;
}
六、采样模型(Sampling Model)
采样模型存储的是"按需抓取"的数据——不是所有数据都存,只有满足特定条件时才记录。
// 采样模型 - 慢查询记录
@Stream(name = "top_n_database_statement", scopeId = 3)
public class TopNDatabaseStatement extends Record {
@Column(columnName = "trace_id")
private String traceId;
@Column(columnName = "id")
@IDColumn
private String id;
@Column(columnName = "statement")
private String statement; // SQL语句
@Column(columnName = "latency")
private long latency; // 执行时间
@Column(columnName = "service_id")
private int serviceId;
@Column(columnName = "time_bucket")
private long timeBucket;
}
// 采样模型 - Profile堆栈快照
@Stream(name = "profile_task_segment_snapshot", scopeId = 3)
public class ProfileThreadSnapshotRecord extends Record {
@Column(columnName = "task_id")
private String taskId;
@Column(columnName = "segment_id")
private String segmentId;
@Column(columnName = "dump_time")
private long dumpTime;
@Column(columnName = "stack_binary")
private byte[] stackBinary; // 线程堆栈数据
}
七、注解驱动的存储字段编排
SkyWalking使用一套优雅的注解体系来声明字段如何存储:
// @Column注解——定义字段的存储行为
public @interface Column {
// 存储中的列名
String columnName();
// 值的数据类型
ValueDataType dataType() default ValueDataType.COMMON_VALUE;
// 如果是指标,使用的聚合函数
Function function() default Function.Avg;
// 是否可搜索
boolean matchQuery() default false;
// 是否可全文搜索
boolean contentQuery() default false;
// 存储类型
StorageType storageType() default StorageType.COMMON;
enum ValueDataType {
COMMON_VALUE, // 普通数值
CONSTANT_VALUE, // 常数值
NOT_VALUE, // 非数值
SAMPLED_RECORD, // 采样记录值
LABELED_VALUE // 标签值
}
}
// @IDColumn——标记主键字段
public @interface IDColumn {}
// @Stream——定义存储流(索引/表)
public @interface Stream {
String name(); // 流名称
int scopeId(); // 范围ID
Class builder(); // Builder类
Class processor(); // Processor类
}
// @MetricsExtension——指标扩展属性
public @interface MetricsExtension {
boolean supportDownSampling() default true;
boolean supportUpdate() default false;
boolean timeRelativeID() default true;
}
八、四种模型的读写流程
+-------------------+
| Agent探针 |
+--------+----------+
|
gRPC上报
|
v
+--------+----------+
| SegmentParser |
| 解析原始Segment |
+--------+----------+
|
+------------+------------+
| |
v v
+-----------+-----------+ +----------+--------+
| 注册模型处理 | | 明细模型处理 |
| | | |
| 1. 检查服务名是否已注册 | | 1. 创建SegmentRecord|
| 2. 检查端点名是否已注册 | | 2. 记录latency等字段|
| 3. 未注册则生成新ID | | 3. 写入存储 |
| 4. 将ID映射放入缓存 | | |
+-----------+-----------+ +---------+---------+
| |
v v
+-----------+-----------+ +---------+---------+
| 后续使用 | | 指标模型处理 |
| | | |
| 其他模型引用注册ID | | 1. 拿到服务ID |
| (如指标模型中的entityId)| | 2. 解析OAL指标定义 |
| | | 3. 执行聚合计算 |
| | | 4. 写入指标数据 |
+-----------------------+ +---------+---------+
|
(慢调用/异常时)
|
v
+---------+---------+
| 采样模型触发 |
| |
| 1. 判断是否满足条件 |
| 2. 抓取堆栈/慢SQL |
| 3. 存储采样数据 |
+--------------------+
图2:四种模型的协同工作流程
九、存储模型的时间窗口分层
指标模型的数据按时间粒度分为三层存储:
原始数据写入(分钟级)
|
v
[分钟窗口] ── TTL: 默认90天
|
| 每小时聚合一次
v
[小时窗口] ── TTL: 默认45天
|
| 每天聚合一次
v
[天窗口] ── TTL: 默认180天
查询时自动选择最合适的粒度:
- 查询最近1小时 → 使用分钟粒度数据
- 查询最近24小时 → 使用小时粒度数据
- 查询最近30天 → 使用天粒度数据
十、总结
SkyWalking的四大存储模型是一种"根据数据使用方式分层存储"的经典设计:
- 注册模型:名→ID映射,体量最小,永久存储
- 明细模型:原始证据,体量最大,较短TTL
- 指标模型:聚合结果,体量中等,是Dashboard的数据源
- 采样模型:按需抓取,体量按规则决定
这套模型体系让SkyWalking既能在Dashboard上秒级响应查询,又能完整保留问题排查所需的原始数据,实现了"快"和"全"的完美平衡。
下一篇文章,我们将深入注册模型和明细模型的细节实现。
上一篇【第48篇】OAP集群规划实战:从理论到生产的容量设计
下一篇【第50篇】注册模型与明细模型:名字的管家和现场的记录官
更多推荐




所有评论(0)