Skip to content

APM 工具


Tracing API / Tracing Agent 概述

Tracing API / Tracing Agent 为开发者提供完全透明的全链路标记传递的框架和服务,分为两部分:

  • Tracing API:框架的上层组件(API 层),面向用户提供标记的插入、更新和查询等基本操作
  • Tracing Agent:框架的底层组件,实现上是一个 Java Agent,挂载到业务服务上,对业务开发者完全透明,实现标记在 JVM 内部和边界上的自动透传

使用文档:全链路标记透传文档


APM 概念

APM(Application Performance Management/Monitoring,应用性能管理/监控)工具的核心目标是确保软件应用的性能、可用性和最终用户体验满足预期。

工作原理

大多数 APM 工具通过以下流程实现监控:

  1. 数据采集:通过在应用程序中部署轻量级代理来实现。代理以非侵入或低侵入方式自动收集应用性能数据。非侵入式通常通过 Java Agent 等技术在字节码层面实现,无需修改业务代码
  2. 数据处理与传输:代理将采集到的数据(响应时间、错误日志、调用关系)进行初步处理和聚合,发送到中心化的 APM 服务器
  3. 存储与分析:APM 服务器接收数据后存储到时序数据库或索引引擎中,利用算法生成拓扑图、追踪调用链、聚合指标等
  4. 可视化与告警:通过 Web 界面以仪表盘、拓扑图、调用链详情等形式展示,并支持设置告警规则

Tracing(链路追踪)

通过一个 traceId 将该请求经过的所有路径串联起来。通过 spanId 表示请求的调用路径关系(例如 spanId=1 是 spanId=1.1 的上游链路,spanId=1.1.1 是 spanId=1.1 的下游链路)。

实现:使用字节码增强技术实现 JVM 内以及跨 JVM 标记透传,所有透传的标记数据存储在线程的 ThreadLocal 中。

每个请求都需要额外消耗少量的内存和 CPU。在高 QPS 的情况下,会增加服务的 GC 和 CPU 压力,对服务的时延和负载可能造成一定影响。


Metrics(指标)

指标信息可以用来配置成指标曲线进行量的统计,包含以下信息:

概念说明
指标名指标的名称,类似数据库中的表名
tags(维度)对指标进行分类,查询时可通过 tags 过滤或 group by(分类数量必须可控,常变化的 id 不适合作为分类)
fields(监控项)一个指标名通常有多个 field,如 cnt、sum、min、max 等

示例:用户下单指标统计

  • 指标名:user.make.order
  • tags:{platform: android, province: 上海}
  • fields:cnt 次数统计

Tracing 与 Metrics 的关联

通过 Tracing 的服务调用图,既可以看到该 RPC 调用的次数、耗时等 metrics 指标信息,也可以查看到耗时最大的那条 Tracing 链路信息。


Logging(日志)

日志信息主要用于进行全文搜索。

实现流程

应用 → log4j2 落盘 → filebeat 采集 → Kafka → Logstash → Elasticsearch → Kibana

Tracing 与 Logging 的关联

在日志信息中记录 traceId:spanId,即可将 Tracing 和 Logging 关联起来:

  1. 用户搜索特定关键字的日志,获取该日志的 traceId:spanId
  2. 根据 traceId:spanId 找到对应的 Tracing 链路信息

主流 APM 工具选型

类型工具特点
商业全能型Dynatrace, AppDynamics, New Relic功能全面,开箱即用,费用较高
开源定制型SkyWalking国产优秀项目,微服务和云原生支持良好,社区活跃
ZipkinTwitter 开源,轻量级,设计简单,易于上手
Pinpoint字节码增强实现无侵入监控
云厂商集成型阿里云 ARMS, 华为云 APM, AWS X-Ray集成度高,部署简便

Java Agent 技术

Java Agent 技术允许在不修改源代码的情况下,对 Java 字节码进行改写,实现对运行在 JVM 上的 Java 程序进行监控、分析和增强。

两种加载方式

1. 启动时加载(静态加载)

在 JVM 启动时通过 -javaagent:your-agent.jar 参数指定 Agent JAR 包。JVM 初始化后执行 premain 方法,注册 ClassFileTransformer

java
public static void premain(String agentArgs, Instrumentation inst) {
    inst.addTransformer(new MyClassFileTransformer());
}

JVM 加载每个类之前,都会回调已注册的 ClassFileTransformer.transform 方法。

2. 运行时加载(动态加载)

通过 Java Attach API 动态地将 Agent 加载到已运行的 JVM 中。需要实现 agentmain 方法:

java
public static void agentmain(String agentArgs, Instrumentation inst) {
    inst.addTransformer(new MyClassFileTransformer());
    inst.retransformClasses(...); // 重新转换已加载的类
}

动态加载可对已经加载的类进行重定义(Redefine)或重转换(Retransform)。

底层基石

层级说明
JVMTI(JVM Tool Interface)JVM 提供的原生编程接口,用于监控和控制 JVM
java.lang.instrumentJDK 标准 API,Java Agent 直接交互的接口,提供 InstrumentationClassFileTransformer
字节码操作库ASM、Javassist 等库提供访问和修改字节码的便捷能力

应用场景

  • 性能监控与分析:在方法执行前后插入代码统计耗时
  • 分布式链路追踪:如 SkyWalking、Pinpoint 等,通过 Agent 自动植入追踪代码
  • 热修复与诊断工具:如 Arthas,运行时诊断 JVM 状态、修改已加载类的行为

注意事项

  • MANIFEST.MF 配置Premain-ClassAgent-Class 必须在 JAR 包中正确配置
  • 性能影响:字节码转换会带来一定开销,需谨慎设计转换逻辑
  • 稳定性:不正确的字节码修改可能导致 JVM 崩溃,务必充分测试

Tracing 实现细节

原理

Agent 包是一个独立的物理文件,放置在服务器文件系统的某个路径下,通过 JVM 参数或运行时指令让目标 Java 进程加载并执行 Agent 包中的代码。其改写/增强方式是使用请求线程的 ThreadLocal 进行传递非业务相关的标识,优点是对业务无侵入、无感知。

Tracing Agent 实现

Tracing Agent 这个 JAR 包实现 Java Agent 字节码增强功能:

  • HTTP 协议:在 Tomcat 流量入口端通过 Filter 方式将标识塞入 ThreadLocal 进行透传
  • RPC 等其他协议:在发送方和接收方同时适配该协议,最终在线程的 ThreadLocal 中拿到传递的标识(如 traceId)

Tracing API 实现

Tracing API 是提供给业务开发者的客户端,开发者可以自己往 ThreadLocal 中塞入自定义的业务标识,然后在下游拿到该标识做处理。

Tracing API 标记在 RPC 协议上的透传基于 Attachment 实现。在 RPC 请求发起端将 TraceContext 上下文中的标记置入 Attachment 携带到下游,Attachment Key 格式为:trace-context-{traceTag}。提供者端找出所有 trace-context-{traceTag} 并放入 TraceContext。

不支持的场景

  • 直接向线程池等待队列添加 Task 任务(未使用 submit 或 execute 方法)
  • 使用 ThreadPoolExecutor 时覆盖了原生的 submit、execute 方法,且未桥接到原生方法上
  • 线程池使用了优先级队列(java.util.PriorityQueue)分发任务
  • ScheduledExecutorService 的周期任务(scheduleAtFixedRatescheduleWithFixedDelay
  • 使用 Hystrix 的请求合并特性
  • 部分异步 IO Callback 场景

日志输出 TraceId

Tracing Agent 支持使用 Log4j2 输出 TraceId 和流量标记。

xml
<Property name="LOG_PATTERN">
[$${env:HOST:-localhost}][${sys:app.name:-myApp}][%d{yyyy-MM-dd HH:mm:ss.SSS}][${LOG_LEVEL_PATTERN}][${sys:PID}][%15.15t][%-40.40c{1.}] : [%X{logId}][%X{TRACE-ID}][%X{TRACE-FLAG}]%m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}
</Property>
  • %X{TRACE-ID}:整个完整调用链路的唯一 TraceId
  • %X{TRACE-FLAG}:流量标记

压测结论

场景额外负载
单机中低 QPS(入 100,出 500)< 2.4%
单机高 QPS(入 1000,出 5000)< 15%

标记透传产生的负载仅和相关流量大小以及标记数量相关,和业务复杂性基本无关。


Metrics 实现

Metrics 打点配合 Tracing 的 traceId 和 Logging 的 ELK,可以实现服务全链路的调用追踪及运行性能分析,并提供服务监控面板和服务告警功能。

三种打点方式

方式特点
Transaction监控一段代码运行情况:运行时间统计、次数、错误次数等
Event记录事件发生次数和错误次数,无运行时间统计
Metrics更专业的打点计数工具,支持多维度,无法与 trace 关联但性能更好

JVM 监控

通过采集 JMX 相关指标实现。以 G1 垃圾收集器为例:

指标含义
G1YoungGenerationYoungGC / MixedGC 发生的次数(均会 STW)
G1OldGenerationG1 的 FullGC,回收整个堆,性能影响较大;有 FullGC 通常意味着内存压力较大

底层实现:Disruptor

基于 CAT 开源库,底层采用 Disruptor 高性能线程间消息传递库。

Disruptor 的核心优化:

  • 环形缓冲区(Ring Buffer):首尾相接的环状数组,内存预先分配,减少 GC 压力
  • 无锁设计与 CAS:通过 CAS 操作协调生产者和消费者,避免锁带来的内核态切换开销
  • 解决伪共享(False Sharing):在关键变量周围增加填充字段,确保每个重要变量独占 CPU 缓存行

时序数据存储

基于开源的单机版 InfluxDB 自研的集群版分布式时序数据库。

时序数据库适合 metrics,不适合 logging 和 tracing。非 metrics 数据可选择更合适的存储,例如 ES。

时序数据的特点:

  • 海量性:数据点连续不断产生,数据量巨大
  • 写多读少:95% 以上操作是写入,读取相对较少
  • 按时间顺序写入:数据点按时间戳递增追加,少有更新或删除
  • 近期热点性:更常查询近期数据
  • 多维性:数据点包含时间戳、值和多个标签

Logging 实现细节

架构

基于传统 ELK 架构(Elasticsearch + Logstash + Kibana):

应用 → log4j2 落盘 → filebeat 采集 → Kafka → Logstash → Elasticsearch → Kibana

日志查询语法

字段类型

字段类型是否分词是否可聚合是否可排序是否可搜索特殊字符备注
text允许超过 256 字符(如 msg, message)
keyword建议不超过 256 字符(如 host, traceId)

单条日志不宜过大,超过 20KB 算大日志。

搜索方式

输入说明
search request无引号,命中包含 search 或 request 的日志
"search request"有引号,命中包含该短语的日志
search AND request同时包含 search 和 request
search AND NOT request包含 search 但不包含 request
traceid:3903220179071*根据 traceId 过滤,末尾加 *
msg:success搜索 msg 字段包含 success

大小写规则

  • 分词字段(msg、message):大小写不敏感
  • 非分词字段(其他字段):大小写敏感(如搜索 level:WARN 不可搜 level:warn

布尔逻辑

支持 ANDORNOT 运算符,大小写均可。

非分词字段

  • 需完全匹配
  • 可用通配符:module:*Ring*
  • 建议加上字段名提高查询性能

特殊字符

+ - = && \|\| > < ! ( ) { } [ ] ^ " ~ * ? : \ /

  • 分词字段中以上字符会被过滤掉
  • 非分词字段中需使用 \ 转义(如 \(1\+1\)\=2

日志落盘要求

要求说明
命名正在写入的以 .log 结尾,分割后的加日期后缀
大小单个文件不超过 256MB
轮转方式推荐 create 模式(创建新文件继续写入)
压缩轮转后不要压缩(收集完即删除)

Log4j2 配置示例

xml
<Properties>
    <Property name="PID">????</Property>
    <Property name="LOG_EXCEPTION_CONVERSION_WORD">%xEx</Property>
    <Property name="LOG_LEVEL_PATTERN">%5p</Property>
    <Property name="LOG_PATTERN">[$${env:HOST:-localhost}][${sys:app.name:-myApp}][%d{yyyy-MM-dd HH:mm:ss.SSS}][${LOG_LEVEL_PATTERN}][${sys:PID}][%15.15t][%-40.40c{1.}] : [%X{logId}][%X{TRACE-ID}][%X{TRACE-FLAG}]%m%n${LOG_EXCEPTION_CONVERSION_WORD}
    </Property>
    <Property name="LOG_PATH">./logs</Property>
</Properties>

Move fast and break things