存储查询 DAO 实现
这一篇解决什么
05-存储层 讲写入侧。这一篇讲查询侧:同一个"读指标"接口,BanyanDB / ES / JDBC 三个后端各用什么机制实现?BanyanDB 用 measure 倒排 + TopN 预聚合 + 原生 trace span 树;ES 用 multi-get 点查 + terms/avg 查询期聚合 + routing;JDBC 用主键 IN + SQL GROUP BY + tag 表 join。读完应该能回答:为什么 BanyanDB 查询最快、ES 次之、JDBC 最慢——以及这三套差别背后各自在押注什么。
为什么三种存储的查询实现差别这么大
先把结论摆出来,再逐个看实现。
三个后端不是"同一种查询的三种写法",而是三种存储哲学在三套查询语义上的投影。差别根源有三层:
-
数据是否原生支持时序预聚合。指标的本质是"每个时间桶、每个实体一个值"。BanyanDB 把这做成了一等公民(measure 模型,写入即按
(time_bucket, entity_id)聚合),查询只是按 entity_id 倒排查一段时间的点。ES 和 JDBC 没有时序原生模型,靠把(time_bucket, entity_id)当作文档 ID / 主键来"伪装"成点查——查询时算出期望的几个桶 ID,直接按 ID 取回,不走聚合。这是三者指标查询性能接近、且都不在查询期聚合的原因。 -
TopN 这类"现算"查询,谁来算、在哪算。BanyanDB 写入时就按
bydb-topn.yaml规则把"最忙的 N 个实体"算好存起来,查询读预聚合结果;没有匹配规则时再退化到查询期meanBy + topN。ES 没有预聚合,靠terms + avg聚合引擎现算。JDBC 也没有,还要按天分表,每张表各自取 TopN 再在 Java 端合并求平均——既慢又不精确。这一层拉开三档差距。 -
trace 这种"树形关联"数据,存成什么样。一条 trace 由多个 span 组成,span 间有父子关系。BanyanDB 把 trace 当一等公民,span 嵌套存在 trace 记录里,一次查询出整棵树。ES 和 JDBC 没有 trace 原生模型,每个 segment(一段)是独立文档 / 行,靠
trace_id字段关联,查一条 trace 要先捞所有 segment 再在应用层拼成树。这决定了 trace 查询的快慢和是否需要二次组装。
一句话:BanyanDB 是为 SkyWalking 量身造的时序+链路库,押注"贴近监控语义";ES 是租来的通用搜索引擎,押注"倒排成熟、能全文搜";JDBC 是拿关系库硬扛,押注"通用、好部署、不引新组件"。 性能差距就是这三个押注的后果。
几个术语,先说清楚
后面反复用到,新人不熟的话先看这里。
-
倒排索引(inverted index):正常查询是"给一个文档,看它有哪些词";倒排是"给一个词,看哪些文档含它"。ES 和 BanyanDB 都给
service_id、trace_id、tags这类可过滤字段建倒排表,值→文档列表。term查询就是查这张表,复杂度 O(匹配数),比扫全表快几个数量级。没有倒排的 JDBC 只能靠 B-tree 索引 +WHERE+ join 模拟。 -
multi-get(多点查):一次请求按一组文档 ID / 主键取回多个文档。ES 的
getClient().ids(...)、JDBC 的where id in (?, ?, ...)都是 multi-get。适合"我已经知道要哪几个 ID,直接拿"的场景,不走聚合,代价 O(ID 数)。 -
GROUP BY 聚合:SQL 的
GROUP BY entity_id把同 entity 的多行折叠成一组,配合avg(value)求每组均值、ORDER BY result DESC LIMIT N取 TopN。这是关系库的标准聚合手段,代价是扫表 + 排序。 -
预聚合 vs 查询期聚合:预聚合是写入时就按规则算好结果存起来,查询直接读;要预先配规则,查询极快但不灵活。查询期聚合是查询来了才扫数据算,灵活但慢,数据量大时贵。
-
routing(ES):ES 写文档时可用一个 routing key 决定文档落哪个分片。SkyWalking 写 trace 用 traceId 作 routing,查询时也传 traceId,ES 就只查一个分片而不是全部分片广播——对按 traceId 点查,省掉 N-1 个分片的无效扫描。

查询层 DAO 抽象解决什么问题
查询层 DAO 抽象(server-core 里的 I*QueryDAO 接口)把"读监控数据"从具体存储引擎里剥出来。上游 GraphQL 查询服务(QueryModule)只认接口——“给我某指标在某时间段、按某实体的时序值”。至于这个值是靠 multi-get、ES terms 聚合、SQL GROUP BY 还是 BanyanDB measure 预聚合算出来,由各存储插件自己决定。这让 SkyWalking 不改查询业务逻辑,就能把同一套 UI 查询落到 ES / BanyanDB / 关系库上。
前置说明(修正任务清单两处与源码不符的表述)
- 接口名是
IRecordsQueryDAO(复数),源码中无IRecordQueryDAO;IMetricsQueryDAO里没有readLabeledValues,实际是readLabeledMetricsValues/readLabeledMetricsValuesWithoutEntity。- ES 查询 DAO 没有用
date_histogram聚合。指标时序靠"文档 ID 多点查"实现,terms/avg只用于 TopN 和拓扑。- PostgreSQL 特化 DAO 没有用 PG 的
jsonb/数组类型(仓库内全局 grepjsonb/array_agg/->>/unnest零命中),只是 SQL 别名/子查询包装的方言适配。
查询型 DAO 接口总览(server-core 定义)
接口目录:oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/storage/query/。这些接口都继承 DAO(空标记接口)或 Service。每个接口带一组 ...Debuggable 默认方法,包一层 DebuggingTraceContext 调试 span——查询层只关心它们最终委派的"真"方法。
接口清单与方法签名
IMetricsQueryDAO — 指标查询(最能体现三后端差异)
IMetricsQueryDAO.java:59。查询侧最核心、也最能体现三后端差异的接口。
| 方法 | 行号 | 语义 |
|---|---|---|
MetricsValues readMetricsValues(MetricsCondition, String valueColumnName, Duration) |
:101 |
读单实体单指标的时序值(每个时间桶一个值) |
List<MetricsValues> readLabeledMetricsValues(condition, valueColumnName, List<KeyValue> labels, Duration) |
:105 |
读带标签的指标(一个指标多个 label 维度,值存在 DataTable 字符串列里) |
List<MetricsValues> readLabeledMetricsValuesWithoutEntity(metricName, valueColumnName, labels, duration) |
:113 |
不指定实体,用来取 label 元数据(限 10 条,常量 METRICS_VALUES_WITHOUT_ENTITY_LIMIT=10 :60) |
List<String> listEntityIdsInRange(metricName, valueColumnName, valueType, duration, limit) |
:163 |
10.5.0 新增,列出某指标在时间区间内有过值的去重 entity_id,供 admin 的 /inspect/entities 用,按最近时间优先 |
HeatMap readHeatMap(condition, valueColumnName, duration) |
:169 |
读热力图(直方图指标,值是 HistogramMetrics.DATASET) |
内嵌 Util 类(:171)做 label 拼装/排序/补默认值,是跨后端共享的纯计算逻辑——三个后端都调它,避免重复实现。
IAggregationQueryDAO — TopN 排序查询
IAggregationQueryDAO.java:36。问"哪些实体最慢/最忙"。
| 方法 | 行号 | ��义 |
|---|---|---|
List<SelectedRecord> sortMetrics(TopNCondition, String valueColumnName, Duration, List<KeyValue> additionalConditions) |
:56 |
按 value 列对实体排序取 TopN。接口注释明确:“基于存储侧聚合,大部分存储支持 groupby/aggregation” |
ITraceQueryDAO / ITraceQueryV2DAO — 链路追踪查询
ITraceQueryDAO.java:35 / ITraceQueryV2DAO.java:31。ITraceQueryV2DAO extends ITraceQueryDAO(V2 是 BanyanDB 用的新版,V1 是 ES/JDBC 用的旧版)。
| 方法 | 行号 | 语义 |
|---|---|---|
TraceBrief queryBasicTraces(duration, minDuration, maxDuration, serviceId, serviceInstanceId, endpointId, traceId, limit, from, TraceState, QueryOrder, List<Tag> tags) |
ITraceQueryDAO.java:114 |
按 trace 列表查询 |
List<SegmentRecord> queryByTraceId(String traceId, @Nullable Duration) |
:130 |
按 traceId 取该 trace 下所有 segment 记录。duration 注释明确"nullable unless for BanyanDB query from cold stage" |
List<SegmentRecord> queryBySegmentIdList(List<String>, Duration) |
:135 |
|
List<SegmentRecord> queryByTraceIdWithInstanceId(List<String>, List<String>, Duration) |
:140 |
|
List<Span> doFlexibleTraceQuery(String traceId) |
:145 |
给"没有 segment 概念的第三方 trace"用 |
V2:List<SpanWrapper> queryByTraceIdV2(traceId, duration) |
ITraceQueryV2DAO.java:54 |
|
V2:TracesQueryResult queryTraces(TraceQueryCondition) |
:56 |
条件对象化、返回带 RetrievedTimeRange |
ITopologyQueryDAO — 拓扑/调用关系查询
ITopologyQueryDAO.java:33。服务、实例、端点、进程四层关系,分 client/server 两端检测。
| 方法 | 行号 | 语义 |
|---|---|---|
loadServiceRelationsDetectedAtServerSide(Duration, List<String> serviceIds) / ...ClientSide(...) |
:190/:196 |
|
| 全局版(不带 serviceIds) | :202/:207 |
|
loadInstanceRelationDetectedAtServerSide(clientServiceId, serverServiceId, Duration) / ...ClientSide(...) |
:213/:221 |
|
loadEndpointRelation(Duration, destEndpointId) |
:228 |
|
loadProcessRelationDetectedAtClientSide(serviceInstanceId, Duration) / ...ServerSide(...) |
:235/:242 |
ILogQueryDAO — 日志查询
ILogQueryDAO.java:40。
| 方法 | 行号 | 语义 |
|---|---|---|
boolean supportQueryLogsByKeywords() |
:42 |
默认 false,只有 ES 翻成 true(LogQueryEsDAO.java:59),表示该后端支持按日志正文关键词检索 |
Logs queryLogs(serviceId, serviceInstanceId, endpointId, TraceScopeCondition relatedTrace, Order, from, limit, Duration, List<Tag> tags, List<String> keywordsOfContent, List<String> excludingKeywordsOfContent) |
:87 |
日志可关联 trace、按 tag 过滤、按正文关键词(含/排除)检索 |
默认方法 parserDataBinary(...) |
:102/:106 |
把 base64 的 LogTags protobuf 反序列化成 KeyValue 列表——跨后端共享 |
其他查询接口速览
| 接口 | 行号 | 语义 |
|---|---|---|
IAlarmQueryDAO |
IAlarmQueryDAO.java:49 |
告警查询,getAlarm(旧)+ queryAlarms(11.0.0 新,支持 entity/layer/ruleName 过滤) |
IBrowserLogQueryDAO |
IBrowserLogQueryDAO.java:30 |
浏览器错误日志,queryBrowserErrorLogs |
IEventQueryDAO |
IEventQueryDAO.java:27 |
事件查询,queryEvents,MAX_SIZE = 100 |
ITagAutoCompleteQueryDAO |
ITagAutoCompleteQueryDAO.java:27 |
标签自动补全,queryTagAutocompleteKeys/Values |
IHierarchyQueryDAO |
IHierarchyQueryDAO.java:26 |
层级关系查询,readAllServiceHierarchyRelations/readInstanceHierarchyRelations |
IRecordsQueryDAO(复数) |
IRecordsQueryDAO.java:37 |
慢查询记录/TopN 采样记录,readRecords |
IZipkinQueryDAO |
IZipkinQueryDAO.java:33 |
Zipkin 兼容查询,getServiceNames/getTrace/getTraces |
接口层的共性设计
三个共性
...Debuggable默认方法模式:每个查询方法都有一个同名...Debuggable默认方法,先DebuggingTraceContext.TRACE_CONTEXT.get(),开了调试就createSpan并把入参拼进 msg,再委派给真方法。调试埋点和业务实现解耦——子类只实现真方法,调试能力由接口默认方法统一注入。Duration贯穿所有接口:Duration封装起止时间戳 + 步长(DownSampling:minute/hour/day)+ 冷数据标记(isColdStage())。所有时间范围查询走Duration,不直接传 long 时间戳,便于三后端统一处理冷热分层。@Nullable Duration的约定:trace 相关方法对duration标了@Nullable,注释反复出现"nullable unless for BanyanDB query from cold stage"——只有 BanyanDB 冷阶段查询需要显式 duration,其余后端用默认最近 24h。
BanyanDB 查询实现
BanyanDB 是 SkyWalking 自研的时序数据库,查询 DAO 散落在 storage-banyandb-plugin 下按数据模型分包:measure/(指标类)、stream/(记录类:log/alarm/事件/profile)、trace/(原生 trace 模型,span 嵌套存储)、顶层 BanyanDBAggregationQueryDAO/BanyanDBRecordsQueryDAO。
查询基础设施 AbstractBanyanDBDAO
oap-server/server-storage-plugin/storage-banyandb-plugin/.../stream/AbstractBanyanDBDAO.java:60。所有 BanyanDB 查询 DAO 的父类,封装四种查询类型:
| 方法 | 查询类型 | 用途 | 行号 |
|---|---|---|---|
query(...) / queryDebuggable(...)(StreamQuery) |
StreamQuery |
stream 模型查询(log/alarm/record) | :73/:111 |
query(...) / queryDebuggable(...)(MeasureQuery) |
MeasureQuery |
measure 模型查询(指标) | :271/:232 |
topNQuery(...) / topNQueryDebuggable(...) |
TopNQuery |
服务端预聚合 TopN | :196/:150 |
queryTrace(...) / queryTraceDebuggable(...) |
TraceQuery |
原生 trace 模型查询 | :349/:310 |
内嵌 QueryBuilder<T> 抽象类(:399)提供条件构造器:eq/ne/gt/gte/lte/in/notIn/match/having,以及 and(List)/or(List) 组合。其中 having(String name, List<String> value)(:406,底层 StringArrayQueryCondition.having)是 BanyanDB 专门用于"在标签数组里包含某 key=value"的原生算子——这是 BanyanDB 倒排索引查 tag 的方式,区别于 ES 的 term 查 tags 字段。
getTimestampRange(Duration)(:478):duration==null 默认取最近 24 小时。
冷数据支持:每个查询类型都有 if (isColdStage) query.setStages(Set.of("cold"))(如 :99、:298、:359),把查询路由到 BanyanDB 冷阶段存储。
BanyanDBMetricsQueryDAO(measure 查询)
oap-server/server-storage-plugin/storage-banyandb-plugin/.../measure/BanyanDBMetricsQueryDAO.java:54。实现 IMetricsQueryDAO。所有方法都用 MeasureQuery。
readMetricsValues(:60):MetadataRegistry.INSTANCE.findMetricMetadata(modelName, step)拿到 measure schema(:65,注释提到对 foreign 指标会合成只读 schema)。queryByEntityID(schema, valueColumnName, duration, entityID)(:261)发起MeasureQuery,条件是query.and(eq(Metrics.ENTITY_ID, entityID))(:267)——按 entity_id 倒排过滤,project 出valueColumnName字段。- 把返回的
DataPoint按timeBucket映射,对duration.assembleDurationPoints()期望的每个时间桶补默认值(:78-90)。
measure 查询不聚合
这里没有聚合。measure 在写入时已经按(time_bucket, entity_id)预聚合好,查询就是按 entity_id 倒排查一个时间段的点。BanyanDB 的 measure 本身就是"时序指标"语义,倒排索引让"按 entity 过滤"是 O(匹配数) 而非 O(全表)。
readLabeledMetricsValues(:108):同样queryByEntityID,但取出DataTable字符串列(:122),再交给跨后端共享的Util.composeLabelValue/Util.sortValues做 label 拆装。listEntityIdsInRange(:173):MeasureQueryproject 出ENTITY_IDtag +valueColumnName,OrderBy DESC(:206)+limit,返回后用LinkedHashSet去重保序(:217)。foreign 指标走synthesizeForeignMetricSchema(:183)。readHeatMap(:225):取HistogramMetrics.DATASET字段,heatMap.buildColumn解析。
别混了 BanyanDBMetricsDAO 与 BanyanDBMetricsQueryDAO
BanyanDBMetricsDAO(measure/BanyanDBMetricsDAO.java:58)实现的是IMetricsDAO(写入/读缓存侧的multiGet/prepareBatchInsert),不是查询侧的IMetricsQueryDAO。两者名字相似但职责不同——一个给 OAL 流处理 worker 读缓存写指标用,一个给 GraphQL 查询读指标展示用。新手最容易混这点。
BanyanDBAggregationQueryDAO(TopN:预聚合 + 查询期聚合双路径)
oap-server/server-storage-plugin/storage-banyandb-plugin/.../BanyanDBAggregationQueryDAO.java:51。实现 IAggregationQueryDAO。sortMetrics(:59)有两条路径:

路径 A:服务端预聚合 TopN(serverSideTopN :122)
schema 里定义了匹配当前查询 tag 组合的 TopNAggregation(:91-92,从 bydb-topn.yaml 配置,由 MetadataRegistry.parseTopNSpecs 解析,见 MetadataRegistry.java:330)时,走 topNQueryDebuggable(:125)。这是 BanyanDB 服务端预先聚合好的 TopN,查询只读结果,极快。TopNQuery 设 Aggregation.Type.MEAN(AbstractBanyanDBDAO.java:208)。
路径 B:查询期聚合 TopN(directMetricsTopN :143)
没有匹配的预聚合规则时,发 MeasureQuery:
query.meanBy(valueColumnName, ImmutableSet.of(Metrics.ENTITY_ID)); // :154 按entity分组求均值
query.topN(condition.getTopN(), valueColumnName); // 或 bottomN,:156/158
查询期在 measure 上做 group-by + mean + topN,由 BanyanDB 引擎现算。
TopN 预聚合 vs 查询期聚合
- 预聚合 TopN:写入时数据库就按规则把"最忙的 N 个实体"算好存起来,查询时直接读结果,适合高频固定看板,但要预先在
bydb-topn.yaml配规则。改规则不影响历史数据(历史按旧规则算好的已落盘)。- 查询期聚合:查询来了才扫数据算 group-by + 排序 + 取 N,灵活但慢,数据量大时贵。
BanyanDB 两者都支持,先试预聚合、不行再查询期——这是它比 ES / JDBC 优越的核心点。代价是要维护
bydb-topn.yaml,规则没配就退化到查询期。
BanyanDBTopologyQueryDAO(measure group by tag)
oap-server/server-storage-plugin/storage-banyandb-plugin/.../measure/BanyanDBTopologyQueryDAO.java:55。实现 ITopologyQueryDAO。关系指标(service/instance/endpoint/process relation)也是 measure。
loadServiceRelationsDetectedAtServerSide(:62)→queryServiceRelation(:106):MeasureQueryproject 出ENTITY_ID+COMPONENT_IDS,条件query.or(in(SOURCE_SERVICE_ID, serviceIds)).or(in(DEST_SERVICE_ID, serviceIds))(:99-100)+query.groupBy({ENTITY_ID, COMPONENT_IDS})(:101)。- 实例关系(
buildInstanceRelationsQuery:155):用and/or组合双向条件(source=client AND dest=server)OR(dest=client AND source=server),query.criteria(or(...))(:175)。 - endpoint/进程关系类似,都靠
groupBytag 去重。
group by tag 去重
BanyanDB 的groupBy(tag set)是原生 measure 操作,按 tag 维度折叠数据点。拓扑查询本质就是"在这段时间内出现过哪些关系实体",group by entity_id 天然去重——同一关系实体出现多少次都被折成一行。这比 ES 的terms聚合去重更直接,因为 group by 是 measure 的原生命令。
BanyanDBTraceQueryDAO(原生 trace 模型——span 嵌套存储)
oap-server/server-storage-plugin/storage-banyandb-plugin/.../trace/BanyanDBTraceQueryDAO.java:59。三后端里 trace 查询差异最大的一处。该类实现 ITraceQueryV2DAO(V2),但通过 BanyanDBStorageProvider 注册为 ITraceQueryDAO.class(BanyanDBStorageProvider.java:173,因为 V2 extends V1)。
-
旧 V1 方法直接抛
UnsupportedOperationException:queryBasicTraces(:71,提示"BanyanDB Trace Model changed, please use queryTraces")、queryByTraceId(:76,提示"please use queryByTraceIdV2")。BanyanDB 升级了 trace 数据模型,不再支持按 segment 列表查。 -
queryTraces(TraceQueryCondition)(:148)—— V2 主查询:用TraceQuery(BanyanDB 原生 trace 查询 API)。条件用eq/gte/lte拼装(traceId、latency 区间、serviceId、instanceId、endpointId、isError),排序OrderBy(START_TIME/LATENCY, DESC),tag 查询用having(SegmentRecord.TAGS, tagsConditions)(:211)。返回TraceQueryResponse,结构是resp.getTraces()返回 trace 列表,每个 trace 的getSpansList()直接给出该 trace 下所有 span(:222、:141)。 -
queryByTraceIdV2(:123):query.and(eq(TRACE_ID, traceId)),返回单个 trace 的所有SpanWrapper(:140-145,从resp.getTraces().get(0).getSpansList()解析)。返回超过 1 个 trace 抛异常(:137)。
trace 的 span 关系查询
一条 trace 由多个 span 组成,span 之间有父子关系。三后端存法不同:
- BanyanDB:trace 是一等公民,span 嵌套存在 trace 记录里。
TraceQuery一次查出来就是"trace→spans 列表"的树状结构,不用二次拼装。这也是旧 V1(按 segment 列表查)被废弃的原因——segment 概念在 BanyanDB trace 模型里被 span 嵌套取代。- ES / JDBC:trace 没有原生模型,每个 segment 是独立文档/行,靠
trace_id字段关联。查一条 trace 要先按trace_id捞出所有 segment 文档,再在应用层拼成 span 树。
buildRecords(:235):把 trace 查询结果转成SegmentRecord(给 profile 等老接口用),从SpanWrapper解析SegmentObjectprotobuf,重新算 serviceId/instanceId/endpointId/latency。注释(:232)说明"only build the fields needed by ProfiledTraceSegments"。
其他 BanyanDB 查询 DAO
| DAO | 实现 | 数据模型 | tag 查询 |
|---|---|---|---|
BanyanDBLogQueryDAO |
ILogQueryDAO |
stream | having(LogRecord.TAGS, tagsConditions),supportQueryLogsByKeywords 默认 false(不支持正文关键词检索) |
BanyanDBAlarmQueryDAO |
IAlarmQueryDAO |
stream | StreamQuery |
BanyanDBRecordsQueryDAO |
IRecordsQueryDAO |
stream | StreamQuery + eq(ENTITY_ID) + OrderBy(valueColumnName) + limit |
BanyanDBTagAutocompleteQueryDAO |
ITagAutoCompleteQueryDAO |
measure | query.groupBy({TAG_KEY})/groupBy({TAG_VALUE}) + eq(TAG_TYPE) 取去重 key/value |
BanyanDBBrowserLogQueryDAO / BanyanDBEventQueryDAO / BanyanDBZipkinQueryDAO / BanyanDBHierarchyQueryDAO / BanyanDBMetadataQueryDAO |
各自接口 | 按 stream/measure 选 | 同上 |
BanyanDB 查询特点小结
- 善用原生能力:measure 查询(预聚合时序点)、
TopNAggregation(服务端预聚合 TopN)、groupBytag(去重/折叠)、having(标签数组倒排查)、TraceQuery(原生 trace span 树)。 - 冷热分层原生支持:所有查询带
isColdStage→setStages("cold")。 - V2 trace 模型:废弃 segment 列表查法,span 嵌套存储,一次查询出整棵 span 树。
- schema 驱动:所有查询先
MetadataRegistry.findMetricMetadata/findRecordMetadata拿 schema,再按 schema 的 tag/field 构建 query——schema 不存在直接抛异常。foreign 指标有synthesizeForeignMetricSchema合成只读 schema 的能力。
Elasticsearch 查询实现
查询 DAO 集中在 oap-server/server-storage-plugin/storage-elasticsearch-plugin/.../query/,都继承 EsDAO。
MetricsQueryEsDAO(按文档 ID 多点查——不做聚合)
oap-server/server-storage-plugin/storage-elasticsearch-plugin/.../query/MetricsQueryEsDAO.java:59。实现 IMetricsQueryDAO。理解 ES 指标查询的关键:ES 指标查询不用 ES 聚合,而是按文档 ID 多点查(multi-get)。

readMetricsValues(:66):duration.assembleDurationPoints()算出期望的时间桶列表(:82)。- 对每个时间桶,用
pointOfTime.id(entityId)拼出文档 ID(:86)。开了 merged table(多指标合并索引)时,用IndexController.generateDocId(metricName, id)给 ID 加指标名前缀(:88)。 - 按
TimeSeriesUtils.queryIndexName把文档 ID 分组到对应时间分片索引(:90-92),得到indexIdsGroup: Map<indexName, List<docId>>。 idsDebuggable(indexIdsGroup)(:97)——调用 ES 的multi-getAPI(getClient().ids(...))按文档 ID 批量取文档。- 取到的文档按 ID 取
realValueColumn字段值,缺失补默认值,再Util.sortValues按时间桶顺序排好。
为什么 ES 不用 date_histogram
SkyWalking 的指标在写入时已经按(time_bucket, entity_id)预聚合好,且把这个组合作为文档 ID。查询时只要算出期望的几个时间桶 ID,直接 multi-get 取回——这比date_histogram聚合便宜得多(O(桶数) 的点查 vs O(扫描全量) 的聚合)。terms/avg聚合只用在 TopN 和拓扑。换句话说,ES 在指标查询上"假装"自己是 KV 存储,绕开了聚合引擎。
readLabeledMetricsValues(:127):同样 multi-get,取回DataTable字符串列。listEntityIdsInRange(:219):这里用Search+range(TIME_BUCKET)+sort(TIME_BUCKET, DESC)+size(limit)(:246-260),扫最近 N 行再客户端LinkedHashSet去重 entity_id(:267)。foreign 指标走合并索引METRICS_LOGIC_TABLE_NAME+term(METRIC_TABLE_NAME, metricName)判别(:240-258)。readHeatMap(:278):同样 multi-get 取dataset字段。buildQuery(:322,protected 辅助):构造range(TIME_BUCKET)+terms(ENTITY_ID)+ 可选term(METRIC_TABLE_NAME)的 bool query,但size(0)(:361)——只用于计数场景,不取文档。
merged table 概念
ES 默认把多个指标类型合并到一个逻辑索引(METRICS_LOGIC_TABLE_NAME),用metric_table字段区分;也支持logicSharding(按指标 stream 类分索引)。IndexController.LogicIndicesRegister.getPhysicalTableName/isMergedTable/getPhysicalColumnName处理这层映射。foreign 指标在 logicSharding 下无法解析物理索引(索引名来自本节点没有的 stream 类),所以:73/:135/:234直接拒绝。
TraceQueryEsDAO(倒排 + 时间范围 + routing)
oap-server/server-storage-plugin/storage-elasticsearch-plugin/.../query/TraceQueryEsDAO.java:58。实现 ITraceQueryDAO(V1,非 V2——ES 没有 V2 trace 模型)。注册为 ITraceQueryDAO.class(StorageModuleElasticsearchProvider.java:236)。
-
queryBasicTraces(:68):构造BoolQueryBuilder:term(RECORD_TABLE_NAME, INDEX_NAME)若 merged(:88)range(TIME_BUCKET).gte().lte()时间范围(:92)range(LATENCY).gte().lte()耗时区间(:96-103)term(SERVICE_ID)/term(SERVICE_INSTANCE_ID)/term(ENDPOINT_ID)/term(TRACE_ID)(:106-116)——全靠倒排索引 term 查询match(IS_ERROR, ...)状态(:119-123)source(...)只取需要的字段(:129-134,减少回表)sort(START_TIME/LATENCY, DESC)(:138-143)- tag 查询:
Query.term(SegmentRecord.TAGS, tag.toString())(:146)——TAGS字段在 ES 里建了倒排,term精确匹配key=value字符串 size(limit).from(from)分页(:149)- 用
TimeRangeIndexNameGenerator按时间范围选索引分片(:152-156)
-
queryByTraceId(:182):term(TRACE_ID, traceId)+size(segmentQueryMaxSize),RoutingUtils.addRoutingValueToSearchParam(searchParams, traceId)(:192)——把 traceId 作为 ES routing 值,让查询只落到存该 trace 的那个分片,避免广播查。
倒排索引查询 + ES routing
- 倒排索引:ES 对
service_id/trace_id/tags这类字段建倒排表(值→文档列表)。term查询就是查倒排表,O(匹配数),比扫全表快几个数量级。SkyWalking 把所有可过滤字段都靠term/range走倒排。- routing:ES 写入时可用一个 routing key 决定文档落哪个分片。SkyWalking 写 trace 时用 traceId 作 routing,查询时同样传 traceId 作 routing,ES 只查一个分片而不是全部分片——对按 traceId 查这种点查,性能提升巨大。代价是文档分布不均(热门 traceId 的分片会偏大),SkyWalking 接受这个权衡。
queryBySegmentIdList(:200):terms(SEGMENT_ID, list)。queryByTraceIdWithInstanceId(:216):bool().must(terms(TRACE_ID, list)).must(terms(SERVICE_INSTANCE_ID, list))。- segment 在 ES 里是独立文档,靠
trace_id关联,应用层buildRecords(:236)逐文档还原SegmentRecord。
AggregationQueryEsDAO(查询期 terms + avg 聚合)
oap-server/server-storage-plugin/storage-elasticsearch-plugin/.../query/AggregationQueryEsDAO.java:49。实现 IAggregationQueryDAO。sortMetrics(:56):
range(TIME_BUCKET)+ 可选term(METRIC_TABLE_NAME)+ 可选term(attr)/mustNot(terms(attr))+terms(additionalCondition)过滤(:61-107)。- 核心聚合(
:109-117):
即Aggregation.terms(ENTITY_ID).field(ENTITY_ID) .order(BucketOrder.aggregation(realValueColumn, asc)) .size(condition.getTopN()) .subAggregation(Aggregation.avg(realValueColumn).field(realValueColumn)) .executionHint(MAP).collectMode(BREADTH_FIRST)terms按 entity_id 分桶 + 子聚合avg求均值 + 按 avg 值排序 + 取 TopN。这是查询期 ES 聚合,与 BanyanDB 的预聚合 TopN 形成对比——没有预聚合可走,只能现算。 - 从
response.getAggregations()解析 buckets(:125-137)。
executionHint(MAP) / collectMode(BREADTH_FIRST)
ES terms 聚合的调优参数。MAP 用内存哈希表收集(适合基数不高),BREADTH_FIRST 广度优先(适合桶多、深子聚合)。SkyWalking 对拓扑/TopN 都用这个组合,是经验值。基数很高(百万级 entity)时 MAP 会吃内存,这是 ES TopN 在大集群下的隐患。
TopologyQueryEsDAO(terms 聚合去重关系实体)
oap-server/server-storage-plugin/storage-elasticsearch-plugin/.../query/TopologyQueryEsDAO.java:51。实现 ITopologyQueryDAO。和 Aggregation 一样用 terms 聚合,但这里是为了去重拿到这段时间出现过哪些关系实体。
loadServiceRelationsDetectedAtServerSide(:58)→buildServiceRelation(:286):terms(ENTITY_ID).size(1000)+ 子聚合terms(COMPONENT_IDS)(:289-299),BREADTH_FIRST+MAP。从 buckets 还原Call.CallDetail。- 实例关系(
:121):单层terms(ENTITY_ID)。 - 进程关系(
:221):terms(ENTITY_ID)+ 子terms(COMPONENT_ID)。 - 条件用
term(SOURCE_SERVICE_ID)/term(DEST_SERVICE_ID)+should组合双向(:174-175)。 size(0)(:63)——不取文档原文,只要聚合结果。
LogQueryEsDAO(倒排 + matchPhrase 关键词)
oap-server/server-storage-plugin/storage-elasticsearch-plugin/.../query/LogQueryEsDAO.java:53。实现 ILogQueryDAO,且 supportQueryLogsByKeywords() 返回 true(:59)——三后端里唯一支持正文关键词检索的。
queryLogs(:64):BoolQueryBuilder+term/range一套倒排过滤(service/instance/endpoint/traceId/segmentId/spanId/tags)。- tag 查询:
Query.term(AbstractLogRecord.TAGS, tag.toString())(:111)——和 trace 一样靠TAGS倒排。 - 关键词检索:
Query.matchPhrase(MatchCNameBuilder.build(CONTENT), content)(:118)——matchPhrase短语匹配,走 ES 全文倒排。排除关键词用mustNot(matchPhrase(...))(:129-130)。MatchCNameBuilder处理列名映射(合并索引下正文列名带前缀)。 sort(TIMESTAMP, DESC/ASC)(:140)+size(limit).from(from)分页(:145)。
ES 查询特点小结
- 指标 multi-get,不用聚合:靠文档 ID(time_bucket+entity_id)点查,避免
date_histogram。 - TopN/拓扑用
terms+avg查询期聚合:无预聚合,靠 ES 聚合引擎现算,调优MAP+BREADTH_FIRST。 - trace/log 全倒排:
term/range/matchPhrase走倒排索引,tags字段也建倒排。 - routing 优化点查:按 traceId 查带 routing,单分片命中。
- merged table 判别:合并索引下用
metric_table/record_table_name字段term过滤区分指标/记录类型。 - 无冷热分层原生支持(不像 BanyanDB 的
setStages),靠TimeRangeIndexNameGenerator按时间选索引分片。
JDBC 查询实现
查询 DAO 在 oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/.../common/dao/(通用,H2/MySQL/PG 共用),PostgreSQL 特化在 postgresql/dao/。
JDBCMetricsQueryDAO(按主键 IN 查——不做聚合)
oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/.../common/dao/JDBCMetricsQueryDAO.java:45。实现 IMetricsQueryDAO。和 ES 思路一样——指标按主键 ID 点查,不聚合。
-
readMetricsValues(:56):tableHelper.getTablesForRead(metricName, startBucket, endBucket)拿到时间范围内要查的物理表(JDBC 按天分表,:64)。duration.assembleDurationPoints()算期望时间桶,TableHelper.generateId(metricName, pointOfTime.id(entityId))拼出主键 ID(:72)。- 对每个表发 SQL:
select id, valueColumnName from table where id in (?, ?, ...)(:76-82)——主键 IN 查询,等价于 ES 的 multi-get,走主键 B-tree 索引。 - 结果按
Util.sortValues排序补默认值。
- foreign 指标:try-catch 跳过不含该 value 列的表(
:97-103)。
-
listEntityIdsInRange(:217):这里用 SQL 聚合:select entity_id, max(time_bucket) as latest_time_bucket ... where table_name=? and time_bucket>=? and time_bucket<=? group by entity_id order by latest_time_bucket desc limit ?(:250-259)。注释(:241-247)解释为什么用GROUP BY+MAX而非SELECT DISTINCT:PostgreSQL 拒绝ORDER BY一个不在DISTINCT列表里的列,所以用 GROUP BY 形式可移植。跨表结果在 Java 端按latest合并(:270)再全局排序取 limit。 -
readHeatMap(:285):同样id in (...)点查取dataset列。
JDBCTraceQueryDAO(SQL WHERE + tag 表 JOIN)
oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/.../common/dao/JDBCTraceQueryDAO.java:62。实现 ITraceQueryDAO(V1)。纯 SQL 字符串拼接,参数化防注入。
queryBasicTraces(:81):- 先校验 tag 是否在
SearchableTracesTags(:97)——只有声明为可搜索的 tag 才能查,否则直接返回空TraceBrief(避免全表扫)。 - 按
getTablesForRead/getTablesWithinTTL遍历时间分表(:113-115)。 - 拼 SQL:
where table_name=? and time_bucket>=? and time_bucket<=? and latency>=? and latency<=? and service_id=? and ... and is_error=1(:138-185),order by start_time desc/order by latency desc(:188/:191),LIMIT ? OFFSET ?(:195,buildLimit:229)。 - tag 查询靠 inner join 单独的 tag 表(
:132、:171-177):inner join segment_tag t0 on main.id=t0.id and t0.tags=?。每个 tag 一次 join。
- 先校验 tag 是否在
JDBC 的 tag 存储模型
SegmentRecord用@SQLDatabase.AdditionalEntity(additionalTables = {ADDITIONAL_TAG_TABLE})(SegmentRecord.java:88,ADDITIONAL_TAG_TABLE = "segment_tag":64)把 tags 拆到独立表segment_tag(id, tags)。主表不存 tags,查 tag 必须 join tag 表。这和 ES(tags 字段建倒排)、BanyanDB(tags 数组 +having)完全不同——JDBC 没有倒排索引,靠额外表 + B-tree 索引 + join 实现 tag 过滤。代价是每个 tag 一次 join,tag 多了 join 数线性增长。
-
queryByTraceId(:236):遍历getTablesWithinTTL所有 TTL 内的表(:237),每表发where table_name=? and trace_id=?。注释里queryBasicTraces的TODO: sort(:226)说明跨表结果排序未在 SQL 层做。没有 ES 那样的 routing——JDBC 无法按 traceId 路由到单表,只能全 TTL 表扫,这是 JDBC trace 查询最慢的根源。 -
queryBySegmentIdList(:257):segment_id in (...)。 -
queryByTraceIdWithInstanceId(:282):trace_id in (...) and service_instance_id in (...)。 -
doFlexibleTraceQuery(:309)返回空。
JDBCAggregationQueryDAO(SQL 子查询 GROUP BY + Java 端合并)
oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/.../common/dao/JDBCAggregationQueryDAO.java:44。实现 IAggregationQueryDAO。sortMetrics(:50):
- 遍历时间分表(
:55)。 buildSQL(:94)构造子查询:
即select result, entity_id from ( select avg(valueColumnName) as result, entity_id from table where table_name=? and time_bucket>=? and time_bucket<=? [and attr=?]... group by entity_id ) as T order by result desc limit topNavg+GROUP BY entity_id+ORDER BY+LIMIT——SQL 查询期聚合,单表内取 TopN。- 跨表合并(
:73-91):因为按天分表,每个表各自取了 TopN,Java 端要groupingBy(SelectedRecord::getId)按实体合并(:79),对同一实体的多个表结果求平均(:84),再按 ASC/DESC 排序、limit(topN)(:90)。
JDBC 分表是性能劣势根源
对比:ES 是单索引一次terms聚合跨所有桶;JDBC 是分表各自聚合再 Java 端合并——分表是 JDBC 性能劣势的根源,跨表 TopN 不可能精确(每表各自的 TopN 合并不等于全局 TopN,除非每表都取足够多)。跨多天查询时,要么每表取的 N 够大(吞吐变差),要么接受近似结果。这是关系库做时序 TopN 的结构性短板。
JDBCTopologyQueryDAO(SQL GROUP BY 去重)
oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/.../common/dao/JDBCTopologyQueryDAO.java:47。实现 ITopologyQueryDAO。loadServiceCalls(:140):
select entity_id, component_ids from table
where table_name=? and time_bucket>=? and time_bucket<=? [and (source=? or dest=?)]
group by entity_id, component_ids
(:172-176)——GROUP BY 去重拿到这段时间出现过的关系实体。实例/端点/进程关系同理(:223-227、:258-263、:295-301),条件用 source=? and dest=? 双向 OR。
PostgreSQL 特化(仅 SQL 方言适配,无 JSON/数组)
oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/.../postgresql/dao/。
PostgreSQLMetricsQueryDAO(:26)继承JDBCMetricsQueryDAO,只重写buildMetricsValueSql(:33):给聚合列加as result别名(PG 对ORDER BY别名要求���格)。PostgreSQLAggregationQueryDAO(:26)继承JDBCAggregationQueryDAO,重写buildMetricsValueSql(:32):用select * from (select avg(...) as result, entity_id from ...)子查询包装。
PG 没用 JSON/数组
对整个 JDBC 插件源码 grepjsonb/array_agg/->>/unnest/@>零命中。PostgreSQLStorageProvider.prepare()(PostgreSQLStorageProvider.java:45)只重注册IMetricsQueryDAO(:50)和IAggregationQueryDAO(:53)两个服务,其余 query DAO 全部继承通用JDBCStorageProvider实现。PG 特化纯粹是 SQL 语法适配(别名/子查询包装),没有用 PG 的 JSON/数组特性。哪怕 PG 有jsonb能存数组 tag,SkyWalking 也没用——保持和 H2/MySQL 同一套segment_tag表 + join 的实现。
JDBC 查询特点小结
- 指标按主键 IN 点查(同 ES multi-get 思路),不聚合。
- TopN/拓扑用 SQL
GROUP BY+avg+ORDER BY查询期聚合,跨天分表在 Java 端合并(不精确)。 - trace/log 全靠 SQL
WHERE,没有倒排索引——tag 查询靠独立 tag 表 + inner join + B-tree。 - 按 traceId 查要扫全 TTL 表(无 routing 概念)。
- 可搜索 tag 受配置限制:
SearchableTracesTags白名单外的 tag 直接拒绝查询。 - PG 特化仅 SQL 方言,无 JSON/数组。
三后端查询实现对比
同一个 readMetricsValues 的三种实现
| 维度 | BanyanDB | Elasticsearch | JDBC |
|---|---|---|---|
| 入口类 | BanyanDBMetricsQueryDAO:60 |
MetricsQueryEsDAO:66 |
JDBCMetricsQueryDAO:56 |
| 查询机制 | MeasureQuery + eq(ENTITY_ID) 倒排过滤 + 时间范围 |
ES multi-get(getClient().ids())按文档 ID 批量取 |
select ... where id in (...) 主键 IN 查询 |
| 是否聚合 | 否(measure 写入已预聚合) | 否(multi-get 点查) | 否(主键点查) |
| 时间桶对齐 | 客户端按 assembleDurationPoints 补默认值 |
同左 | 同左(Util.sortValues) |
| 分片/分表 | 按 group/stage(冷热) | TimeSeriesUtils.queryIndexName 按时间分索引 |
tableHelper.getTablesForRead 按天分表 |
三者都不在查询期聚合指标
因为指标在写入时已按(time_bucket, entity_id)预聚合,且该组合就是主键/文档 ID。查询本质都是"算出期望的时间桶 ID,按 ID 点查回填"。差异只在存储引擎的 API:BanyanDB 用 measure 查询语言、ES 用 multi-get、JDBC 用WHERE id IN。这一层三者性能接近,差距在 TopN 和 trace 才拉开。
谁用原生聚合、谁用查询期计算(TopN)
| 维度 | BanyanDB | Elasticsearch | JDBC |
|---|---|---|---|
| 入口类 | BanyanDBAggregationQueryDAO:59 |
AggregationQueryEsDAO:56 |
JDBCAggregationQueryDAO:50 |
| 预聚合 TopN | ✅ TopNAggregation(bydb-topn.yaml,服务端预聚合,:99) |
❌ 无 | ❌ 无 |
| 查询期聚合 | ✅ directMetricsTopN:meanBy+topN(:154-158) |
✅ terms+avg+BucketOrder(:110-114) |
✅ avg+GROUP BY+ORDER BY+LIMIT(:104-137) |
| 跨分片/分表 | 单 measure 内原生跨桶 | 单索引内 ES 聚合引擎 | 分表各自聚合 + Java 端合并求平均(:79-90),不精确 |
性能差异根源:BanyanDB 有预聚合 TopN 路径,高频看板可走预聚合(O(预聚合结果大小));ES 和 JDBC 只能查询期现算。JDBC 还因为按天分表,跨表 TopN 要 Java 端合并,数据量大或跨多天时既慢又不精确。ES 单索引聚合引擎相对最优(无分表),但仍比 BanyanDB 预聚合慢。一句话:BanyanDB 用空间换时间(预存结果),ES 用算力换灵活(现算),JDBC 受限于分表结构两头不沾。
trace 查询:谁用倒排、谁用 SQL where
| 维度 | BanyanDB | Elasticsearch | JDBC |
|---|---|---|---|
| 入口类 | BanyanDBTraceQueryDAO:59(V2) |
TraceQueryEsDAO:58(V1) |
JDBCTraceQueryDAO:62(V1) |
| 查询机制 | 原生 TraceQuery(trace→spans 嵌套,一次出 span 树) |
BoolQueryBuilder + term/range/match 倒排 + sort + 分页 |
SQL WHERE 字符串拼接 + ORDER BY + LIMIT/OFFSET |
| trace 列表查询 | queryTraces(TraceQueryCondition)(:148),返回 TracesQueryResult 含 span 树 |
queryBasicTraces(:68),返回 TraceBrief(segment 列表) |
queryBasicTraces(:81),返回 TraceBrief |
| 按 traceId 取详情 | queryByTraceIdV2(:123),一次取整棵 span 树 |
queryByTraceId(:182),term(TRACE_ID) + routing 优化 |
queryByTraceId(:236),扫全 TTL 表 where trace_id=? |
| tag 查询 | having(TAGS, ["k=v"])(:211,原生数组倒排查) |
term(TAGS, "k=v")(:146,倒排字段精确匹配) |
inner join segment_tag on id=id and tags=?(:132,独立表 + B-tree) |
| 关键词/正文 | 不支持 | ✅ matchPhrase(CONTENT)(LogQueryEsDAO:118) |
不支持 |
| 冷数据 | ✅ setStages("cold") |
❌(靠时间索引分片) | ❌(靠 TTL 分表) |
| span 关系组装 | 无需组装(span 嵌套存储) | 应用层从 segment 文档拼 | 应用层从 segment 行拼 |
性能差异根源:
- BanyanDB trace 是原生模型,span 嵌套,一次查询出整棵树,且
having走原生倒排——最快。 - ES 靠倒排 + routing,按 traceId 查能单分片命中,很快;但 span 树要应用层拼。
- JDBC 没有倒排也没有 routing,按 traceId 查要扫所有 TTL 表(最慢);tag 查询要 join tag 表;且 tag 必须在
SearchableTracesTags白名单内。
性能差异根源总结

- 指标查询:三者都靠"预聚合 + 主键点查",性能接近。差异主要在冷热分层(BanyanDB 原生支持)和分片策略。
- TopN 聚合:BanyanDB 有预聚合 TopN(最快);ES 查询期聚合(次之,单索引);JDBC 查询期聚合 + 分表 Java 合并(最慢且不精确)。
- trace 查询:BanyanDB 原生 span 树(最快,无需二次组装);ES 倒排 + routing(快,需组装);JDBC 全表扫 + join(最慢,需组装)。
- tag 查询:BanyanDB
having(原生数组倒排);ESterm(倒排字段);JDBC join tag 表(无倒排,B-tree)。 - 关键词检索:仅 ES
matchPhrase支持正文检索;BanyanDB/JDBC 不支持。 - 冷数据:仅 BanyanDB 原生
setStages支持;ES/JDBC 靠时间分片/分表间接实现。
关键文件速查
| 子系统 | 关键类 | 路径 |
|---|---|---|
| 查询接口 | 各 I*QueryDAO |
oap-server/server-core/.../storage/query/*.java |
| BanyanDB 查询 | AbstractBanyanDBDAO | oap-server/server-storage-plugin/storage-banyandb-plugin/.../stream/AbstractBanyanDBDAO.java |
| BanyanDBMetricsQueryDAO | 同目录 measure/BanyanDBMetricsQueryDAO.java |
|
| BanyanDBAggregationQueryDAO | .../banyandb/BanyanDBAggregationQueryDAO.java |
|
| BanyanDBTraceQueryDAO | .../banyandb/trace/BanyanDBTraceQueryDAO.java |
|
| ES 查询 | MetricsQueryEsDAO | oap-server/server-storage-plugin/storage-elasticsearch-plugin/.../query/MetricsQueryEsDAO.java |
| TraceQueryEsDAO | 同目录 TraceQueryEsDAO.java |
|
| AggregationQueryEsDAO | 同目录 AggregationQueryEsDAO.java |
|
| LogQueryEsDAO | 同目录 LogQueryEsDAO.java |
|
| JDBC 查询 | JDBCMetricsQueryDAO | oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/.../common/dao/JDBCMetricsQueryDAO.java |
| JDBCTraceQueryDAO | 同目录 JDBCTraceQueryDAO.java |
|
| JDBCAggregationQueryDAO | 同目录 JDBCAggregationQueryDAO.java |
|
| PostgreSQL 特化 | .../postgresql/dao/PostgreSQLMetricsQueryDAO.java、PostgreSQLAggregationQueryDAO.java |
未确认事项
- PostgreSQL 的
PostgreSQLTableInstallerDDL 是否用 PG 特殊类型(jsonb/数组)建表,本次未读 DDL 源码,标注未确认。但查询 DAO 层确定没用 PG JSON/数组。- ES 是否在其它查询 DAO(如
MetadataQueryEsDAO、profiling 相关)用date_histogram,本次未全量检索;但本篇重点的 5 个 DAO(Metrics/Trace/Aggregation/Log/Topology)确认未用date_histogram。- BanyanDB
TraceQuery服务端具体如何存 span 嵌套(protobuf 结构细节),本次只看到客户端TraceQueryResponse.getTraces().getSpansList(),服务端存储格式未深查。
全系列回顾
到这里,SkyWalking OAP 的核心子系统、数据模型、协议层、Profiling、查询 DAO 都拆解完了。回到 README 看整体地图,或回看任意一篇:
- 00-整体架构与端到端数据流 / 01-模块系统与启动流程 / 02-数据采集层-Receiver
- 03-分析引擎与四大DSL / 04-流处理引擎-Worker链 / 05-存储层
- 06-查询层与告警 / 07-集群协调与动态配置
- 08-数据模型与Scope体系 / 09-协议层与Agent采集
- 10-Profiling体系
心智模型
把查询 DAO 想成"展厅的三种导览系统":BanyanDB 是为 SkyWalking 量身定制的智能导览——measure 货架按 ID 直取、TopN 区有预分拣榜单、trace 区有原生族谱树、标签有数组倒排;ES 是租来的通用搜索引擎——指标靠 ID 点查、TopN 靠现场聚合、trace 靠倒排+routing、还能全文搜日志;JDBC 是拿关系库当仓库——指标靠主键 IN、TopN 靠 GROUP BY+跨表合并、trace 靠全表扫+join tag 表。同一份"查询菜单"(接口),三种仓库各有各的取法,性能差距源自谁更贴近时序+链路的原生语义。