Skip to content

transactionInfo 接口代码逻辑分析

接口路径: GET /transactionInfo
所在类: com.apm.starry.web.controller.BusinessTransactionController
功能概述: 根据 traceId 查询一笔完整的分布式交易(Trace)详情,包含调用栈、应用拓扑图等信息,用于"交易快照"页面展示。


1. 接口签名与请求参数

java
@RequestMapping(value = "/transactionInfo", method = RequestMethod.GET)
@ResponseBody
public TransactionInfoViewModel transactionInfo(
    @RequestParam("traceId") String traceIdParam,
    @RequestParam(value = "focusTimestamp", required = false, defaultValue = "0") long focusTimestamp,
    @RequestParam(value = "agentId", required = false) String agentId,
    @RequestParam(value = "spanId", required = false, defaultValue = "-1") long spanId,
    @RequestParam(value = "v", required = false, defaultValue = "0") int viewVersion
)
参数名类型是否必填默认值说明
traceIdString-交易ID字符串,格式为 agentId^agentStartTime^transactionSequence
focusTimestamplong0焦点时间戳,用于确定当事务包含多个 Span 时,以哪个 Span 为基准展示
agentIdStringnullAgent 标识,辅助定位焦点 Span
spanIdlong-1Span 标识,辅助定位焦点 Span
vint0视图版本号,影响应用拓扑图的构建方式

2. 整体执行流程

mermaid
flowchart TD
    A["接收请求参数"] --> B["解析 traceId"]
    B --> C["查询 Span 列表"]
    C --> D["构建调用树 CallTree"]
    D --> E["构建应用拓扑图 ApplicationMap"]
    D --> F["创建 RecordSet 调用栈记录集"]
    E --> G["组装 TransactionInfoViewModel"]
    F --> G
    G --> H["返回 JSON 响应"]

3. 详细步骤分析

3.1 解析 traceId

java
final TransactionId transactionId = TransactionIdUtils.parseTransactionId(traceIdParam);
  • 将前端传入的 traceIdParam 字符串解析为 TransactionId 对象。
  • TransactionId 包含三个字段:agentIdagentStartTimetransactionSequence
  • 同时生成 transactionStr 用于后续数据库查询:
java
String transactionStr = transactionId.getAgentId() + "^" + transactionId.getAgentStartTime() + "^" + transactionId.getTransactionSequence();

3.2 查询 Span 列表

java
List<SpanBo> spanBoList = this.spanService.selectSpanList(transactionId, focusTimestamp);

调用链路: SpanService.selectSpanList(TransactionId, long)SpanService.getSpanListNew(Map)

核心逻辑 (getSpanListNew):

  1. 构建查询参数: 以 transaction_str 为条件,时间范围为 focusTimestamp ± 12小时,查询 ClickHouse 的 Span 表。
  2. 查询所有 SpanBo: 调用 spanDao.getSpanListNew(params) 获取原始数据。
  3. 拆分数据:
    • type == 0 → 主 Span(按 collectorAcceptTime 降序)
    • type != 0 → SpanChunk(按 collectorAcceptTime 升序)
  4. 查询 SpanEvent: 批量查询所有 Span 和 SpanChunk 关联的事件数据。
  5. 组装调用关系: 将 SpanChunkBo 挂载到对应的 SpanBo 下,将 SpanEventBo 挂载到对应的 Span/SpanChunk 下。
  6. 构建 Annotation: 为每个 Span/SpanEvent 解析注解信息(支持 JSON 格式和简单 key-value 格式)。
  7. 数据清洗: 过滤掉没有 SpanChunk、SpanEvent 和 Annotation 的垃圾数据。

3.3 构建调用树 (CallTree)

java
final SpanResult spanResult = this.spanService.selectSpan(spanBoList, focusTimestamp);
final CallTreeIterator callTreeIterator = spanResult.getCallTree();

调用链路: SpanService.selectSpan(List<SpanBo>, long)SpanService.order(...) → 返回 SpanResult

selectSpan 核心逻辑:

  1. 空判断: 若 spanBoList 为空,返回 ERROR 状态的空结果。
  2. 排序构建调用树: 调用 order() 方法将平铺的 Span 列表组织成树形调用结构,生成 CallTreeIterator
  3. 元数据翻译(使用 CompletableFuture 并发执行 5 个任务):
任务方法说明
task1transitionDynamicApiIdapiId 翻译为可读的 API 方法名(查询 api_meta_data 表)
task2transitionSqlIdsqlId 翻译为完整 SQL 语句(查询 sql_meta_data 表)
task3transitionMongoJson翻译 MongoDB 查询的 JSON 表示
task4transitionCachedString翻译缓存的字符串元数据(查询 string_meta_data 表)
task5transitionException处理异常信息的翻译

性能优化: 上述 5 个翻译任务通过线程池 querySpanChuckTask 并发执行,且内部对批量 ID 做了分页查询(每次最多 1000 个),避免 SQL IN 子句过长。

3.4 构建应用拓扑图 (ApplicationMap)

java
ApplicationMap map = filteredMapService.selectApplicationMap(spanBoList, viewVersion);

调用链路: FilteredMapService.selectApplicationMap(List<SpanBo>, int)

核心逻辑:

  1. 创建 FilteredMapBuilder,传入应用工厂、服务类型注册表、时间范围和视图版本。
  2. 调用 addTransaction(spanBoList) 将 Span 数据添加到构建器。
  3. 调用 build() 生成 FilteredMap
  4. 通过 createMap() 方法:
    • 构建节点直方图(NodeHistogramFactory):统计各应用节点的响应时间分布。
    • 构建服务实例列表(ServerInstanceListFactory):获取各 Agent 实例信息。
    • 调用 ApplicationMapBuilder 构建最终的 ApplicationMap,包含 节点 (Nodes)连线 (Links)
  5. 若配置了 serverMapDataFilter,对拓扑图数据做过滤。

返回的 ApplicationMap 包含:

  • Collection<Node> nodes — 应用节点(包含应用名、服务类型、Agent 信息、响应直方图等)
  • Collection<Link> links — 调用关系连线(包含源/目标应用、调用次数、响应时间等)

3.5 创建 RecordSet

java
RecordSet recordSet = this.transactionInfoService.createRecordSet(callTreeIterator, focusTimestamp, agentId, spanId);

调用链路: TransactionInfoService.createRecordSet(...)

核心逻辑:

  1. 查找焦点 ViewPoint:

    • 遍历 alignList,依次匹配 focusTimestampagentIdspanId
    • 若无法精确匹配,返回第一个 Span 作为 fallback。
    • 从 ViewPoint 中提取 agentIdapplicationIdapplicationName
  2. 计算时间范围: 从 alignList 的首条记录获取 startTimeendTime

  3. 检查日志事务标记: 遍历所有 Span 检查是否有 LoggingInfo.LOGGED 标记。

  4. 填充调用栈记录 (SpanAlignPopulate.populateSpanRecord):

    • 遍历 CallTreeIterator 的每个节点。
    • 使用 RecordFactory 为每个节点创建 Record,包含:
      • 基本 Record: 方法名、耗时、参数等。
      • 异常 Record: 若有异常,追加异常记录。
      • 注解 Record: 追加 SQL、参数等注解信息。
      • 远程地址 Record: 追加 REMOTE_ADDRESS
      • 端点 Record: 追加 ENDPOINT
  5. 标记焦点记录: 在 RecordList 中标记与 ViewPoint 匹配的记录为 focused = true

RecordSet 结构:

字段说明
startTime调用栈开始时间
endTime调用栈结束时间
agentIdAgent 标识
applicationId应用 ID
applicationName应用名称(RPC 参数或显示参数)
beginTimestampViewPoint 的开始时间戳
recordList调用栈记录列表
loggingTransactionInfo是否有日志事务标记

3.6 组装 ViewModel 并返回

java
return new TransactionInfoViewModel(
    transactionStr, spanId,
    map.getNodes(), map.getLinks(),
    recordSet,
    spanResult.getTraceState(),
    logConfiguration
);

4. 返回数据结构 (TransactionInfoViewModel)

JSON 字段类型来源说明
transactionIdString入参解析交易ID字符串
spanIdlong入参Span标识
applicationNameStringRecordSet应用名称
agentIdStringRecordSetAgent标识
applicationIdStringRecordSet应用ID
callStackStartlongRecordSet.startTime调用栈起始时间
callStackEndlongRecordSet.endTime调用栈结束时间
completeStateStringSpanResult.traceState链路完整状态(如 COMPLETE / ERROR)
loggingTransactionInfobooleanRecordSet是否包含日志事务信息
disableButtonMessageStringLogConfiguration日志按钮禁用提示消息
callStackIndexMap<String, Integer>静态定义调用栈字段索引映射
callStackList<CallStack>RecordSet.recordList 转换调用栈数据(序列化为数组格式)
applicationMapDataMap<String, List<Object>>Nodes + Links应用拓扑图数据

CallStack 字段说明(共 26 个字段):

索引字段名说明
0depth调用深度
1begin开始时间
2end结束时间
3excludeFromTimeline是否从时间线中排除
4applicationName应用名称
5tab缩进层级
6id记录ID
7parentId父记录ID
8isMethod是否为方法调用
9hasChild是否有子节点
10title方法/API名称
11arguments参数信息
12executeTime执行时间(格式化)
13gap间隔时间
14elapsedTime耗时(ms)
15barWidth时间条宽度(百分比)
16executionMilliseconds执行毫秒数
17simpleClassName简短类名
18methodType方法类型代码
19apiTypeAPI类型
20agentAgent名称
21isFocused是否为焦点记录
22hasException是否有异常
23isAuthorized是否已授权
24fullApiDescription完整API描述
25exceptionClassRoute异常类路径

5. 核心依赖关系图

mermaid
graph LR
    Controller["BusinessTransactionController"] --> SpanService["SpanService"]
    Controller --> TransactionInfoService["TransactionInfoService"]
    Controller --> FilteredMapService["FilteredMapService"]
    Controller --> LogConfiguration["LogConfiguration"]

    SpanService --> SpanDao["SpanDao (ClickHouse)"]
    SpanService --> ApiMetaDataDao["ApiMetaDataDao"]
    SpanService --> SqlMetaDataDao["SqlMetaDataDao"]
    SpanService --> StringMetaDataDao["StringMetaDataDao"]

    TransactionInfoService --> AnnotationKeyMatcherService["AnnotationKeyMatcherService"]
    TransactionInfoService --> ServiceTypeRegistryService["ServiceTypeRegistryService"]
    TransactionInfoService --> RecordFactory["RecordFactory"]

    FilteredMapService --> FilteredMapBuilder["FilteredMapBuilder"]
    FilteredMapService --> ApplicationMapBuilder["ApplicationMapBuilder"]
    FilteredMapService --> AgentInfoService["AgentInfoService"]

6. 关键数据库交互

步骤DAO 方法数据源说明
查询 Span 列表SpanDao.getSpanListNewClickHouse按 transaction_str 和时间范围查询
查询 SpanEventSpanDao.getSpanEventStrListClickHouse按 Span ID 批量查询事件
翻译 API 元数据ApiMetaDataDao.getApiMetaDataBatchClickHouse批量查询 api_meta_data 表
翻译 SQL 元数据SqlMetaDataDao.getSqlMetaDataBatchClickHouse批量查询 sql_meta_data 表
翻译字符串元数据StringMetaDataDao.getStringMetaDataBatchClickHouse批量查询 string_meta_data 表

7. 性能优化措施

  1. 并发元数据翻译: 5 个翻译任务(API、SQL、MongoDB、CachedString、Exception)通过 CompletableFuture 并发执行。
  2. 批量查询: 所有元数据查询都做了分批处理(每批最多 1000 条),避免 SQL IN 子句过长导致报错。
  3. 共享线程池: 使用名为 querySpanChuckTaskThreadPoolTaskExecutor,避免频繁创建线程池导致资源泄漏。
  4. SpanEvent 数量限制: 当 Span ID 超过 2000 个时进行截断,防止查询过于庞大。
  5. 时间范围限定: 查询时间窗口为 focusTimestamp ± 12小时,避免全表扫描。

8. 异常处理与容错

  • TransactionId 解析失败: TransactionIdUtils.parseTransactionId 解析异常将直接抛出,由全局异常处理器捕获。
  • 空 Span 列表: 若查询不到任何 Span,selectSpan 返回 ERROR 状态的空 CallTreeIterator
  • API 元数据未找到: 记录到 Redis 缓存 (SqlAndApiNotFoundCache),并在 Annotation 中标记 ERROR_API_METADATA_NOT_FOUND
  • SQL 元数据未找到: 同上,标记 SQL-ID not found
  • .NET 探针兼容: 当 api_meta_data 查不到时,尝试从 Annotation(key=12) 或 SpanStacktrace(key=9030) 中提取 API 名称。

9. 该接口的调用方

调用位置场景
前端"交易快照"页面直接发起 GET 请求展示交易详情
BusinessTransactionController.getFundamental()根因分析时内部调用,获取交易调用栈后发送给 RCA 引擎
OpenAnalysisTraceController.transactionInfo()开放 API 中的相同功能实现

10. 涉及数据库表总结

该接口一共涉及 5 张 ClickHouse 表 + 1 张 MySQL 表 + Redis,共计 6 个数据存储交互

ClickHouse 表(5 张)

#表名调用步骤DAO 方法用途
1span3.2 查询 Span 列表SpanDao.getSpanListNew查询主 Span 和 SpanChunk 原始数据(type=0 为主 Span,type!=0 为 SpanChunk),按 transaction_str 和时间范围查询
2span_event3.2 查询 SpanEventSpanDao.getSpanEventStrList查询 Span 关联的事件数据(方法调用、SQL 执行等),按 parent_id 批量查询
3api_meta_data3.3 元数据翻译 (task1)MetaDataInfoDao.selectApiMetaDataBatchapiId 翻译为可读的 API 方法签名(如 com.example.UserService.getUser()
4sql_meta_data3.3 元数据翻译 (task2)MetaDataInfoDao.selectSqlMetaDataBatchsqlId 翻译为完整的 SQL 语句文本
5string_meta_data3.3 元数据翻译 (task4)MetaDataInfoDao.selectStringMetaDataBatch将缓存的 stringId 翻译为原始字符串值(如方法参数、HTTP Header 等)

MySQL 表(1 张)

#表名调用步骤DAO 方法用途
6agent_info3.4 构建应用拓扑图AgentInfoService.getAgentsByApplicationNameWithoutStatus构建 ApplicationMap 时查询 Agent 实例信息,用于填充拓扑图中的服务器实例列表

Redis

#用途调用步骤说明
7缓存未找到的元数据 ID3.3 元数据翻译api_meta_datasql_meta_data 中找不到对应 ID 时,通过 SqlAndApiNotFoundCache 写入 Redis 缓存,用于后续排查或补偿

表交互流程图

mermaid
flowchart LR
    subgraph ClickHouse
        T1["span"]
        T2["span_event"]
        T3["api_meta_data"]
        T4["sql_meta_data"]
        T5["string_meta_data"]
    end
    subgraph MySQL
        T6["agent_info"]
    end
    subgraph Redis
        R1["SqlAndApiNotFoundCache"]
    end

    A["transactionInfo 接口"] --> T1
    T1 -->|"Span ID 列表"| T2
    T1 -->|"apiId 列表"| T3
    T1 -->|"sqlId 列表"| T4
    T1 -->|"stringId 列表"| T5
    A -->|"构建拓扑图"| T6
    T3 -.->|"未找到时写入"| R1
    T4 -.->|"未找到时写入"| R1