【SkyWalking从入门到精通】第41篇:OAL是什么:观察力分析语言的前世今生
上一篇【第40篇】手把手开发OAP Server扩展打造企业微信告警通知下一篇【第42篇】OAL语法全解析声明式指标计算的优雅之舞一、开篇为什么SkyWalking不直接写SQL如果你刚接触SkyWalking可能会疑惑“为什么不直接写SQL来统计指标MySQL/ES都支持聚合查询啊”或者“用Drools规则引擎不香吗声明式规则多优雅”这两个问题的答案正好揭示了OAL存在的全部理由。想象一个场景每秒有1000个服务调用请求通过探针上报到OAP。每个请求都有响应时间、状态码、调用链等数据。如果每个请求都实时地写一条SQLINSERT INTO metrics ... ON DUPLICATE KEY UPDATE ...光SQL解析和执行的开销就足以把数据库打死。每秒1000个调用请求 | v ------------ ----------------- | 如果直接写SQL | | 每秒1000次SQL执行 | ------------ | 存储层被打爆 | ----------------- | v ------------ ----------------- | 用OAL定义指标 | | 内存中流式聚合 | ------------ | 批量写入存储 | ----------------- 图1OAL vs 直接SQL——性能天差地别二、设计哲学OAL的三个核心定位2.1 定位一编译型脚本语言不是解释型OAL不是解释执行的脚本那会让每次聚合都解释一遍规则而是编译型的。它的编译过程是.oal文件 Antlr4语法解析 Java字节码 运行时 endpoint_avg -- 解析为AST -- 生成Metrics类 -- 直接执行 from Endpoint. (抽象语法树) (编译一次) (零开销解析) metricSum(...);这意味着**.oal文件只编译一次运行时执行的是编译后的Java字节码性能接近原生代码**。2.2 定位二流式计算语义不是批量查询SQL的思维模型是查询已有的数据-- SQL思维我有什么数据统计一下SELECTservice_name,AVG(response_time)FROMtracesWHEREtimenow()-5mGROUPBYservice_name;OAL的思维模型是数据流过时实时计算// OAL思维数据来了当场算endpoint_avgfromEndpoint.avg(latency);// ^^^^ ^^^// 数据流 聚合算子// 每来一条数据 当场计算2.3 定位三指标导向不是规则导向Drools解决的是规则匹配问题如果AB且CD则触发动作X而OAL要解决的是指标计算问题对数据流持续做AVG/SUM/PERCENTILE。维度SQLDroolsOAL计算模式查历史规则匹配流式计算性能模型存储层聚合内存推理内存流式聚合主要用途报表分析风控/稽核可观测指标扩展性数据库决定规则数限制水平扩展实时性分钟级延迟秒级延迟实时数据到达即计算三、OAL的数据处理模型流式管道OAL背后是一个经典的流式计算管道探针数据 | v ---------- ---------- ---------- ---------- | Source | -- | Filter | -- | Aggregator| -- | Sink | | 数据源 | | 过滤器 | | 聚合器 | | 输出 | | | | | | | | | | Endpoint | | name NOT | | avg() | | 分钟窗口 | | Service | | LIKE %| | sum() | | 小时窗口 | | Relation | | errortrue| | percentile| | 天窗口 | ---------- ---------- ---------- ---------- | | | | | 数据从探针 | 过滤掉不需要 | 在内存中做 | 窗口闭合后 | 上报而来 | 的数据 | 数学聚合 | 写入存储 图2OAL的流式计算管道模型每个OAL指标定义本质上就是声明了一个这样的管道。以最经典的响应时间指标为例// 端点平均响应时间指标 endpoint_avg from Endpoint.avg(latency); // 翻译成管道语义 // Source: 所有Endpoint类型的Segment数据 // Filter: 无全部参与计算 // Agg: 对latency字段做avg聚合 // Sink: 每1分钟/1小时/1天输出一次聚合结果四、OAL的编译产物生成的Java类长什么样我们来看一个具体的例子。假设你有这样一个OAL定义service_resp_time from Service.latency;编译后会生成这样一个Java类简化版/** * 由OAL编译器自动生成 * 来源: service.oal 第15行 * 指标名: service_resp_time */Stream(nameservice_resp_time,scopeId0,builderServiceRespTimeMetrics.Builder.class,processorServiceRespTimeMetrics.Processor.class)MetricsExtension(supportDownSamplingtrue,supportUpdatefalse,timeRelativeIDtrue)publicclassServiceRespTimeMetricsextendsMetricsimplementsWithMetadata,LongValueHolder{// 指标字段 Column(columnNamevalue,dataTypeColumn.ValueDataType.COMMON_VALUE,functionFunction.Avg)privatelongvalue;Column(columnNametotal,dataTypeColumn.ValueDataType.COMMON_VALUE)privatelongtotal;Column(columnNamesum,dataTypeColumn.ValueDataType.COMMON_VALUE)privatelongsum;// 存储字段 Column(columnNameentity_id)IDColumnprivateStringentityId;Column(columnNametime_bucket)privatelongtimeBucket;// 聚合处理器 publicstaticclassProcessor{/** * 流式聚合的核心方法 * 每收到一条探针数据这个方法就被调用一次 */publicvoidcombine(SourceFromSourcesource,// 原始数据如Endpoint的latencyArgServiceRespTimeMetricsmetrics,// 当前窗口的累积值ContextMetricsContextcontext){// 实现AVG聚合// Avg total_sum / total_countmetrics.total;// 计数器1metrics.sumsource.getLatency();// 累加总和metrics.valuemetrics.sum/metrics.total;// 计算平均值}/** * 窗口闭合时调用计算最终结果 */publicvoidcombine(SourceFromServiceRespTimeMetricssource,ArgServiceRespTimeMetricstarget){// 合并两个时间窗口的数据用于L2聚合target.totalsource.total;target.sumsource.sum;target.valuetarget.sum/target.total;}}// Builder模式 publicstaticclassBuilderimplementsStorageBuilderServiceRespTimeMetrics{OverridepublicServiceRespTimeMetricsstorage2Entity(MapString,ObjectdbMap){// 从存储反序列化ServiceRespTimeMetricsmetricsnewServiceRespTimeMetrics();metrics.value((Number)dbMap.get(value)).longValue();metrics.total((Number)dbMap.get(total)).longValue();metrics.sum((Number)dbMap.get(sum)).longValue();metrics.entityId(String)dbMap.get(entity_id);metrics.timeBucket((Number)dbMap.get(time_bucket)).longValue();returnmetrics;}OverridepublicMapString,Objectentity2Storage(ServiceRespTimeMetricsmetrics){// 序列化到存储MapString,ObjectmapnewHashMap();map.put(value,metrics.value);map.put(total,metrics.total);map.put(sum,metrics.sum);map.put(entity_id,metrics.entityId);map.put(time_bucket,metrics.timeBucket);returnmap;}}}五、OAL在SkyWalking中的角色全景图---------------------------------------------------------------- | SkyWalking 数据流 | | | | [Agent探针] | | | | | | Segment数据 (gRPC) | | v | | [Receiver模块] | | | | | | 原始Segment | | v | | [Analysis模块] --- OAL在这里发挥作用 | | | | | | ----------- ----------- ----------- | | | | 指标计算 | | 拓扑构建 | | 告警检测 | | | | | (OAL驱动) | | | | | | | | ---------- ----------- ----------- | | | | | | | | Metrics对象 (每秒数千条) | | v v | | [L1聚合 - 每个OAP节点本地] | | | | | | 分钟级聚合结果 | | v | | [L2聚合 - 集群协调后] | | | | | | 小时/天级聚合结果 | | v | | [Storage模块] -- Elasticsearch / MySQL / ... | | | | | | | | [Query模块] -- UI / API 查询 | | | ---------------------------------------------------------------- 图3OAL在SkyWalking数据流中的关键位置六、OAL与传统技术的本质区别6.1 为什么不用SQLSQL方案的问题-- 如果用SQL做实时聚合每5秒就要执行一次SELECTservice_id,COUNT(*)astotal,AVG(latency)asavg_latency,PERCENTILE(latency,0.99)asp99FROMsegment_tracesWHEREtime_bucketNOW()-INTERVAL5SECONDGROUPBYservice_id;问题清单每5秒全表扫描一次segment_traces该表可能已有上亿行数据库压力与数据量成正比不是O(1)的PERCENTILE等高阶聚合不支持或性能极差不同存储ES/MySQL/H2SQL方言不同无法统一6.2 为什么不用DroolsDrools方案的问题// Drools规则示例rule慢服务告警when $m:Metric(nameresponse_time,value3000)then alarmService.fireAlarm($m);end问题清单Drools引擎开销大Rete网络构建和维护规则数量增多后推理性能急剧下降计算AVG和匹配规则是不同层次的问题Drools不处理流式窗口、不管理存储写入七、OAL的编译流水线service.oal文件 (文本开发者编写) | v ------------------ | Antlr4 Lexer | 词法分析拆分成Token | (OALLexer) | endpoint_avg, , from, Endpoint, ... ----------------- | v ------------------ | Antlr4 Parser | 语法分析构建AST | (OALParser) | ----------------- | v ------------------ | OALVisitor | 语义分析类型检查、引用解析 | | 生成中间表示AnalyzeDefinition ----------------- | v ------------------ | 代码生成器 | 生成Java源码 | (CodeGenerator) | 使用ASM或字符串模板 ----------------- | v ------------------ | 类加载器 | 加载到JVM | (ClassLoader) | 注册到AnalysisModule ------------------ 图4OAL的编译器流水线八、常见误区澄清误区1“OAL是一种查询语言”错。OAL是数据流处理定义语言不是查询语言。它定义的是数据到达时怎么计算不是怎么从存储里查询。误区2“OAL可以替代PromQL”不完全对。OAL负责指标的计算和生成PromQL负责指标的查询和可视化。两者关注点不同但可以配合使用SkyWalking支持Prometheus格式暴露指标。误区3“学会OAL需要懂编译器原理”不需要。OAL的语法设计极度简化通常只有一行代码。真正需要理解编译原理的是那些想理解源码或自定义扩展的开发者。九、总结OAL是SkyWalking为了高效指标计算而自研的领域特定语言。它的存在理由很朴素SQL太慢、Drools太重、写Java太啰嗦。核心要点OAL是编译型脚本语言运行时零解析开销OAL基于流式计算模型数据到达即计算OAL生成Java字节码性能接近手写代码OAL关注指标计算不是规则匹配或数据查询下一篇我们将全面解析OAL的语法从基本的指标定义到高级的聚合函数。上一篇【第40篇】手把手开发OAP Server扩展打造企业微信告警通知下一篇【第42篇】OAL语法全解析声明式指标计算的优雅之舞