上一篇【第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篇】注册模型与明细模型:名字的管家和现场的记录官


Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐