1. APM系统


1.1. APM系统概述

  • APM (Application Performance Management) 即应用性能管理系统,是对企业系统即时监控以实现对应用程序性能管理和故障管理的系统化的解决方案。应用性能管理,主要指对企业的关键业务应用进行监测、优化,提高企业应用的可靠性和质量,保证用户得到良好的服务,降低 IT 总拥有成本。
  • APM系统是可以帮助理解系统行为、用于分析性能问题的工具,以便发生故障的时候,能够快速定位和解决问题。

    1.2. 分布式链路追踪

  • 随着分布式系统和微服务架构的出现,一次用户的请求会经过多个系统,不同服务之间的调用关系十分复杂,任何一个系统出错都可能影响整个请求的处理结果。以往的监控系统往往只能知道单个系统的健康状况、一次请求的成功失败,无法快速定位失败的根本原因。

  • 除此之外,复杂的分布式系统也面临这下面这些问题:
    • 性能分析:一个服务依赖很多服务,被依赖的服务也依赖了其他服务。如果某个接口耗时突然变长了,那未必是直接调用的下游服务慢了,也可能是下游的下游慢了造成的,如何快速定位耗时变长的根本原因呢?
    • 链路梳理:需求迭代很快,系统之间调用关系变化频繁,靠人工很难梳理清楚系统链路拓扑(系统之间的调用关系)。
  • 为了解决这些问题,Google 2010 年发布文章 Dapper,大规模分布式系统的跟踪系统,之后各个互联网公司都参照 Dapper 的思想推出了自己的分布式链路跟踪系统,而这些系统就是分布式系统下的 APM 系统。

  • OpenTracing 通过提供平台无关、厂商无关的 API,使得开发人员能够方便的添加(或更换)追踪系统的实现。

  • 中文文档:
    • https://github.com/opentracing-contrib/opentracing-specification-zh/
    • https://wu-sheng.gitbooks.io/opentracing-io/

      1.4. 主流开源APM产品

      | | CAT | Zipkin | Apache SkyWalking | | —- | —- | —- | —- | | 调用链可视化 | 有 | 有 | 有 | | 聚合报表 | 非常丰富 | 少 | 较丰富 | | 服务依赖图 | 简单 | 简单 | 好 | | 埋点方式 | 侵入 | 侵入 | 非侵入,运行期字节码增强 | | VM 指标监控 | 好 | 无 | 有 | | 告警支持 | 有 | 无 | 有 | | 多语言支持 | Java、C/C++、Node.js、Python、Go | 丰富 | Java、LUA、Python、.NET、Node.js、PHP、GO2Sky | | 存储机制 | MySQL(报表)、本地文件、HDFS(调用链) | 可选 inMemory、MySQL、ES(生产)、Cassandra(生产) | ES、MySQL、Sharding Sphere、TiDB、H2 | | 社区支持 | 主要在国内,点评/美团 | 文档丰富,国外主流 | Apache 支持,国内社区好 | | 国内案例 | 点评、携程、陆金所、拍拍贷等 | 京东、阿里定制不开源等 | 华为、小米、当当、微众银行等 | | 源头 | eBay CAL | Google Dapper | Google Dapper | | 同类产品 | 暂无 | Uber Jaeger、Spring Cloud Sleuth | Naver Pinpoint | | 出现年份 | 2011 | 2012 | 2015 | | Github Stars | 14.1k | 13.3k | 14.3k | | 亮点 | 企业生产级,报表丰富 | 社区生态好 | 非侵入,Apache 背书 | | 不足 | 用户体验一般,社区一般 | APM 报表能力弱 | 时间不长,文档一般,仅限中文社区 |

2.1. 概述

  • SkyWalking:一个开源的可观测平台, 用于从服务和云原生基础设施收集, 分析, 聚合及可视化数据。
  • SkyWalking 提供了一种简便的方式来清晰地观测分布式系统, 甚至横跨多个云平台。SkyWalking 更是一个现代化的应用程序性能监控(Application Performance Monitoring)系统, 尤其专为云原生、基于容器的分布式系统设计。

    2.2. 特点

  • 多语言自动探针,Java, C# , Node.js , Go , PHP 以及 Nginx LUA(包括 Python 和 C++ 调用的 SDK 捐献)。

  • 多种监控手段,语言探针和 service mesh。
  • 轻量高效。不需要额外搭建大数据平台。
  • 模块化架构。UI、存储、集群管理多种机制可选。
  • 支持告警。
  • 优秀的可视化解决方案。

    2.3. 核心概念

  • 服务(Service):表示对请求提供相同行为的一组工作负载。在使用打点代理或 SDK 的时候,你可以定义服务的名字。SkyWalking 还可以使用在 Istio 等平台中定义的名称。

  • 服务实例(Service Instance):上述的一组工作负载中的每一个工作负载称为一个实例。就像 Kubernetes 中的 pods 一样,服务实例未必就是操作系统上的一个进程。但当你在使用打点代理的时候,一个服务实例实际就是操作系统上的一个真实进程。
  • 端点(Endpoint):对于特定服务所接收的请求路径,如 HTTP 的 URI 路径和 gRPC 服务的类名 + 方法签名。

    2.4. 架构

    SkyWalking 逻辑上分为四部分:探针、平台后端、存储和用户界面。

image.png

  • 探针:基于不同的来源可能是不一样的,但作用都是收集数据,将数据格式化为 SkyWalking 适用的格式。
  • 平台后端:支持数据聚合,数据分析以及驱动数据流从探针到用户界面的流程。分析包括 SkyWalking 原生追踪和性能指标以及第三方来源,包括 Istio 及 Envoy telemetry、Zipkin 追踪格式化等。 你甚至可以使用 Observability Analysis Language 对原生度量指标用于扩展度量的计量系统 自定义聚合分析。
  • 存储:通过开放的插件话的接口存放 SkyWalking 数据。你可以选择一个既有的存储系统,如 ElasticSearch、H2 或 MySQL 集群(Sharding-Sphere 管理),也可以选择自己实现一个存储系统。
  • UI:一个基于接口高度定制化的 Web 系统,用户可以可视化查看和管理 SkyWalking 数据。

    3. 环境搭建


3.1. 单机环境

  1. 搭建 ES 单机环境
  2. 下载 SkyWalking
  3. 切换到 root 用户,解压 SkyWalking 压缩包(以 8.1.0 为例)。

    1. # 切换到root用户
    2. su root
    3. # 切换到skywalking目录
    4. cd /usr/local/skywalking
    5. #解压压缩包
    6. tar -zxvf apache-skywalking-apm-8.1.0.tar.gz
  4. 修改 SkyWalking 存储的数据源配置。修改文件 apache-skywalking-apm-bin/config/application.yml

    • 注释掉默认的 H2 数据源。

      # h2: 
      #     driver: ${SW_STORAGE_H2_DRIVER:org.h2.jdbcx.JdbcDataSource} 
      #     url: ${SW_STORAGE_H2_URL:jdbc:h2:mem:skywalking-oap-db} 
      #     user: ${SW_STORAGE_H2_USER:sa} 
      #     metadataQueryMaxSize: ${SW_STORAGE_H2_QUERY_MAX_SIZE:5000} 
      # mysql: 
      #     metadataQueryMaxSize: ${SW_STORAGE_H2_QUERY_MAX_SIZE:5000}
      
    • 配置 ES 数据源。

      storage:
      elasticsearch: 
         nameSpace: ${SW_NAMESPACE:""} 
         clusterNodes: ${SW_STORAGE_ES_CLUSTER_NODES:localhost:9200} 
         protocol: ${SW_STORAGE_ES_HTTP_PROTOCOL:"http"} 
         trustStorePath: ${SW_SW_STORAGE_ES_SSL_JKS_PATH:"../es_keystore.jks"} 
         trustStorePass: ${SW_SW_STORAGE_ES_SSL_JKS_PASS:""} 
         user: ${SW_ES_USER:""} 
         password: ${SW_ES_PASSWORD:""} 
         indexShardsNumber: ${SW_STORAGE_ES_INDEX_SHARDS_NUMBER:2} 
         indexReplicasNumber: ${SW_STORAGE_ES_INDEX_REPLICAS_NUMBER:0} 
         # Those data TTL settings will override the same settings in core module. 
         recordDataTTL: ${SW_STORAGE_ES_RECORD_DATA_TTL:7} # Unit is day 
         otherMetricsDataTTL: ${SW_STORAGE_ES_OTHER_METRIC_DATA_TTL:45} # Unit is day 
         monthMetricsDataTTL: ${SW_STORAGE_ES_MONTH_METRIC_DATA_TTL:18} # Unit is month
         # Batch process setting, refer to https://www.elastic.co/guide/en/elasticsearch/client/java-api/5.5/java-docs-bulk-processor.html 
         bulkActions: ${SW_STORAGE_ES_BULK_ACTIONS:1000} # Execute the bulk every 1000 requests 
         flushInterval: ${SW_STORAGE_ES_FLUSH_INTERVAL:10} # flush the bulk every 10 seconds whatever the number of requests 
         concurrentRequests: ${SW_STORAGE_ES_CONCURRENT_REQUESTS:2} # the number of concurrent requests
         metadataQueryMaxSize: ${SW_STORAGE_ES_QUERY_MAX_SIZE:5000} 
         segmentQueryMaxSize: ${SW_STORAGE_ES_QUERY_SEGMENT_SIZE:200}
      
    • 默认使用了 localhost 下的 ES,所以我们可以不做任何处理,直接进行使用。

  5. 启动 OAP 程序(/bin/startup.sh 可同时启动 backend 和 ui)。

    bin/oapService.sh
    
  6. 如有需要,修改 UI 系统配置文件(webapp/webapp.yml)。

  7. 启动 UI 系统(/bin/startup.sh 可同时启动 backend 和 ui)。

    /bin/webappService.sh
    
  8. 访问系统。

    http://127.0.0.1:8080
    

3.2. 集群环境

  • 详细文档查看《SkyWalking 集群环境搭建》。

    3.3. Docker 安装单机环境

  1. 创建 docker 网络

    docker network create skywalking-network
    
  2. docker 中安装 ES 环境

    docker run -d --name es7.8.1 --net skywalking-network --network-alias es7.8.1 -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:7.8.1
    
  3. docker 中安装 OAP 服务

    docker run --name oap8.1.0 -d --net skywalking-network --network-alias oap8.1.0 -p 1234:1234 -p 11800:11800 -p 12800:12800 -e SW_STORAGE=elasticsearch7 -e SW_STORAGE_ES_CLUSTER_NODES=es7.8.1:9200 apache/skywalking-oap-server:8.1.0-es7
    
  4. docker 中安装 UI 服务

    docker run --name sw-ui8.1.0 -d --net skywalking-network -p 8080:8080 -e SW_OAP_ADDRESS=oap8.1.0:12800 apache/skywalking-ui:8.1.0
    

4. agent 使用


4.1. IDEA 使用

  • idea 运行配置中配置以下参数。

image.png

  • -javaagent 是 agent jar 的位置,该 jar 包可在 skywalking 安装包中找到。
  • SW_AGENT_NAME 是该服务在 skywalking 中的服务名称。
  • SW_AGENT_COLLECTOR_BACKEND_SERVICES 是 skywalking 服务的采集数据端口。

    4.2. JAR 命令启动

    java -javaagent:E:\Workspaces\LearnWorkspace\SkyWalking示例\skywalking\apache-skywalking-apm-bin-es7\agent\skywalking-agent.jar -Dskywalking.agent.service_name=skywalking-user -Dskywalking.collector.backend_service=127.0.0.1:11800 -jar user-1.0.0-SNAPSHOT.jar
    

5. 配置覆盖


  • 默认情况下,SkyWalking为agent提供了agent.config配置文件。
  • 配置覆盖意味着用户可以通过系统属性或 agent 选项覆盖配置文件中的配置。

    5.1. 系统属性

  • 使用 skywalking. + 配置文件中(agent.config)的配置名作为系统配置项来进行覆盖。

    • 使用前缀的原因:agent 系统属性是和目标应用共享的,加前缀是为了避免冲突。
  • 比如对于 agent.service_name 配置的覆盖。
    -Dskywalking.agent.service_name=skywalking_demo
    

5.2. agent 选项

  • 在JVM参数的agent路径后面添加选项。

    -javaagent:/path/to/skywalking-agent.jar=[option1]=[value1],[option2]=[value2]
    
  • 例如,通过下面配置覆盖 agent.application_codelogging.level

    -javaagent:/path/to/skywalking-agent.jar=agent.service_name=skywalking_demo,logging.level=debug
    
  • 如果在选项或选项值中有分隔符( , 或者 = ),应该用引号包起来。

    -javaagent:/path/to/skywalking-agent.jar=agent.ignore_suffix='.jpg,.jpeg'
    

5.3. 系统环境变量

  • 修改配置文件中的值。

    agent.service_name=${SW_AGENT_NAME:Your_ApplicationName}
    collector.backend_service=${SW_AGENT_COLLECTOR_BACKEND_SERVICES:127.0.0.1:11800}
    logging.level=${SW_LOGGING_LEVEL:INFO}
    
  • 如果 SW_AGENT_NAME 环境变量在你的操作系统中已存在,并且它的值为 skywalking-demo ,那么这里的 agent.service_name的值将会被覆写为 skywalking-demo , 否则, 它将会被设置成 Your_ApplicationName

  • 另外,占位符嵌套也是支持的,比如 ${SW_AGENT_NAME:${ANOTHER_AGENT_NAME:Your_ApplicationName}}。 在这种情况下,如果 SW_AGENT_NAME 环境变量不存在,但是 ANOTHER_AGENT_NAME 环境变量存在,并且它的值为 skywalking-demo , 那么这里的 agent.service_name 的值将会被覆写为skywalking-agent-demo , 否则, 它将会被设置成 Your_ApplicationName

    5.4. 覆盖优先级

  • 探针配置 > 系统配置 > 系统环境变量 > 配置文件中的值。

    6. 获取追踪ID


  • 添加工具包依赖。

    <dependency>
      <groupId>org.apache.skywalking</groupId>
      <artifactId>apm-toolkit-trace</artifactId>
      <version>${skywalking.version}</version>
      <scope>provided</scope>
    </dependency>
    
  • 使用 TraceContext.traceId() 返回 traceId。

    @GetMapping("/traceId")
    public String traceId() {
      return TraceContext.traceId();
    }
    

7. 过滤指定端点


  • 需要使用到 apm-trace-ignore-plugin 插件。
  • skywalking/agent/optional-plugins/apm-trace-ignore-plugin-8.1.0.jar 拷贝到 skywalking/agent/plugins 目录下。
  • 启动项目添加 -Dskywalking.trace.ignore_path 参数标识需要过滤的请求。

    java -javaagent:E:\Workspaces\LearnWorkspace\SkyWalking示例\skywalking\apache-skywalking-apm-bin-es7\agent\skywalking-agent.jar -Dskywalking.agent.service_name=skywalking-user -Dskywalking.collector.backend_service=127.0.0.1:11800 -Dskywalking.trace.ignore_path=/exclude -jar user-1.0.0-SNAPSHOT.jar
    
  • 以上路径配置支持 Ant Path 表达式,例如:

    • /path/* :* 匹配 0 或者任意数量的字符。
    • /path/** :** 匹配0或者更多的目录。
    • /path/? :? 匹配任何单字符。

      8. 打印日志信息


  • 需要添加依赖包。

    <dependency>
      <groupId>org.apache.skywalking</groupId>
      <artifactId>apm-toolkit-trace</artifactId>
      <version>${skywalking.version}</version>
      <scope>provided</scope>
    </dependency>
    
  • 使用 ActiveSpan.errorActiveSpan.infoActiveSpan.debug 打印日志。

    @GetMapping("/printLog")
    public String printLog() {
      // 使当前链路报错,并且提示报错信息
      ActiveSpan.error(new RuntimeException("test error throwable."));
    
      // 打印 info 信息
      ActiveSpan.info("test info msg.");
    
      // 打印 debug 信息
      ActiveSpan.debug("test debug msg.");
    
      return "success";
    }
    
  • 结果

image.png

  • 异常信息有点长,没有截全。

9. 告警功能


9.1. 告警功能简介

  • Skywalking每隔一段时间根据收集到的链路追踪的数据和配置的告警规则(如服务响应时间、服务响应时间百分比)等,判断如果达到阈值则发送相应的告警信息。发送告警信息是通过调用 webhook 接口完成,具体的 webhook 接口可以使用者自行定义,从而开发者可以在指定的 webhook 接口中编写各种告警方式,比如邮件、短信等。告警的信息也可以在 RocketBot 中 查看到。
  • 以下是默认的告警规则配置,位于/skywalking/config/alarm-settings.yml 文件中: ```yaml rules:

    Rule unique name, must be ended with _rule.

    service_resp_time_rule: metrics-name: service_resp_time op: “>” threshold: 1000 period: 10 count: 3 silence-period: 5 message: Response time of service {name} is more than 1000ms in 3 minutes of last 10 minutes. service_sla_rule:

    Metrics value need to be long, double or int

    metrics-name: service_sla op: “<” threshold: 8000

    The length of time to evaluate the metrics

    period: 10

    How many times after the metrics match the condition, will trigger alarm

    count: 2

    How many times of checks, the alarm keeps silence after alarm triggered, default as same as period.

    silence-period: 3 message: Successful rate of service {name} is lower than 80% in 2 minutes of last 10 minutes service_resp_time_percentile_rule:

    Metrics value need to be long, double or int

    metrics-name: service_percentile op: “>” threshold: 1000,1000,1000,1000,1000 period: 10 count: 3 silence-period: 5 message: Percentile response time of service {name} alarm in 3 minutes of last 10 minutes, due to more than one condition of p50 > 1000, p75 > 1000, p90 > 1000, p95 > 1000, p99 > 1000 service_instance_resp_time_rule: metrics-name: service_instance_resp_time op: “>” threshold: 1000 period: 10 count: 2 silence-period: 5 message: Response time of service instance {name} is more than 1000ms in 2 minutes of last 10 minutes database_access_resp_time_rule: metrics-name: database_access_resp_time threshold: 1000 op: “>” period: 10 count: 2 message: Response time of database access {name} is more than 1000ms in 2 minutes of last 10 minutes endpoint_relation_resp_time_rule: metrics-name: endpoint_relation_resp_time threshold: 1000 op: “>” period: 10 count: 2 message: Response time of endpoint relation {name} is more than 1000ms in 2 minutes of last 10 minutes

    Active endpoint related metrics alarm will cost more memory than service and service instance metrics alarm.

    Because the number of endpoint is much more than service and instance.

    #

    endpoint_avg_rule:

    metrics-name: endpoint_avg

    op: “>”

    threshold: 1000

    period: 10

    count: 2

    silence-period: 5

    message: Response time of endpoint {name} is more than 1000ms in 2 minutes of last 10 minutes

webhooks:

- http://127.0.0.1/notify/

- http://127.0.0.1/go-wechat/


- 以上文件定义了默认的 4 种规则:
   - 最近 3 分钟内服务的平均响应时间超过 1 秒。
   - 最近 2 分钟服务成功率低于 80%。
   - 最近 3 分钟 p50、p75、p90、p95、p99 至少一个条件超过 1 秒。
   - 最近 2 分钟内服务实例的平均响应时间超过 1 秒。
   - 最近 2 分钟内数据库平均响应时间超过 1 秒。
   - 最近 2 分钟内端点响应时间超过 1 秒。
- 规则中的参数属性如下:
| 属性 | 含义 |
| --- | --- |
| metrics-name | oal 脚本中的度量名称 |
| threshold | 阈值,与 metrics-name 和下面的比较符号相匹配 |
| op | 比较操作符,可以设定 >、<、= |
| period | 多久检查一次当前的指标数据是否符合告警规则,单位分钟 |
| count | 达到多少次后,发送告警消息 |
| silence-period | 在多久之内,忽略相同的告警消息 |
| message | 告警消息内容 |
| include-names | 本规则告警生效的服务列表 |

- 所有的度量名称被定义在 OAL 脚本中,位置在 `/skywalking根目录/config/oal/*.oal` 。
<a name="c88c3d65"></a>
#### 9.2. 告警测试代码

- 模拟一个超时告警接口。
```java
@GetMapping("/timeout")
public String timeout() {
    try {
        Thread.sleep(1500);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    return "timeout";
}
  • 多次调用之后,可以在告警页面看到如下信息。

image.png

9.3. 告警处理 webhook

告警的消息通过 HTTP 请求进行发送,请求方法是 POST,Content-Type 是 application/json,JSON 格式基于 List<org.apache.skywalking.oap.server.core.alarm.AlarmMessage。

  • 创建一个消息类 ```java import lombok.Data;

@Data public class AlarmMessage {

private int scopeId;
private String scope;
// 目标 Scope 的实体名称.
private String name;
// Scope 实体的 ID.
private String id0;
private String id1;
// 您在 alarm-settings.yml 中配置的规则名.
private String ruleName;
// 报警消息内容.
private String alarmMessage;
// 告警时间, 位于当前时间与 UTC 1970/1/1 之间.
private long startTime;

}


- 创建一个 webhook 的测试接口
```java
/**
 * 告警
 * @author 华夏紫穹
 */
@RestController
public class WebHookController {

    List<AlarmMessage> alarmList = new ArrayList<>();

    /** 产生告警时调用的地方 */
    @PostMapping("/webhook")
    public void webhook(@RequestBody List<AlarmMessage> alarmList) {
        this.alarmList = alarmList;
    }

    /** 展示告警信息 */
    @GetMapping("/show")
    public List<AlarmMessage> show() {
        return alarmList;
    }

}

9.4. 部署测试

  • 修改告警规则配置文件 /skywalking/config/alarm-settings.yml

    webhooks: 
    - http://127.0.0.1:9092/webhook
    
  • 重启 OAP 服务。

  • 启动项目,调用几次 [http://127.0.0.1:9092/timeout](http://127.0.0.1:9092/timeout) ,等一会就会出现警告。
  • 通过调用 [http://127.0.0.1:9092/show](http://127.0.0.1:9092/show) 就可以看到当前的告警信息。
    image.png

    10. java agent 的原理


10.1. java agent 是什么?

  • Java agent 是 java 命令的一个参数。参数 javaagent 可以用于指定一个 jar 包。
    • 这个 jar 包的 MANIFEST.MF 文件必须指定 Premain-Class 项。
    • Premain-Class 指定的那个类必须实现 premain() 方法。
  • 当Java 虚拟机启动时,在执行 main 函数之前,JVM 会先运行 -javaagent 所指定 jar 包内 Premain-Class 这个类的 premain 方法 。

    10.2. 如何使用 java agent?

  • 定义一个 MANIFEST.MF 文件,必须包含 Premain-Class 选项,通常也会加入Can-Redefifine-Classes 和 Can-Retransform-Classes 选项。

  • 创建一个Premain-Class 指定的类,类中包含 premain 方法,方法逻辑由用户自己确定。
  • 将 premain 的类和 MANIFEST.MF 文件打成 jar 包。
  • 使用参数 -javaagent: jar 包路径启动要代理的方法。

    10.3. 搭建 java agent 工程

    10.3.1. 新建 PreMainAgent 类

    ```java public class PreMainAgent {

    /**

    • 在这个 premain 函数中,开发者可以进行对类的各种操作。
    • 1、agentArgs 是 premain 函数得到的程序参数,随同 “– javaagent”一起传入。与 main 函数不同的是,
    • 这个参数是一个字符串而不是一个字符串数组,如果程序参数有多个,程序将自行解析这个字符串。 *
    • 2、Inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。*
    • java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,也是这个包的核心部分,
    • 集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。 */ public static void premain(String agentArgs, Instrumentation inst) { System.out.println(“premain 方法执行 1”); System.out.println(agentArgs); }

      /**

    • 如果不存在 premain(String agentArgs, Instrumentation inst)
    • 则会执行 premain(String agentArgs) */ public static void premain(String agentArgs) { System.out.println(“premain 方法执行 2”); System.out.println(agentArgs); }

}


- 类中提供两个静态方法,方法名均为premain,不能拼错。
<a name="46a8d9c5"></a>
##### 10.3.2. pom 添加打包插件
```xml
<build>
    <plugins>
        <plugin>
            <artifactId>maven-assembly-plugin</artifactId>
            <configuration>
                <appendAssemblyId>false</appendAssemblyId>
                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
                <archive>
                    <!--自动添加META-INF/MANIFEST.MF -->
                    <manifest>
                        <addClasspath>true</addClasspath>
                    </manifest>
                    <manifestEntries>
                        <Premain-Class>com.zp.skywalking.javaagentdemo.PreMainAgent</Premain-Class>
                        <Agent-Class>com.zp.skywalking.javaagentdemo.PreMainAgent</Agent-Class>
                        <Can-Redefine-Classes>true</Can-Redefine-Classes>
                        <Can-Retransform-Classes>true</Can-Retransform-Classes>
                    </manifestEntries>
                </archive>
            </configuration>
            <executions>
                <execution>
                    <id>make-assembly</id>
                    <phase>package</phase>
                    <goals>
                        <goal>single</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>
  • 该插件会在自动生成META-INF/MANIFEST.MF文件时,帮我们添加agent相关的配置信息。

    10.3.3. 打包
  • 使用 maven 的package 命令进行打包。

    10.3.4. 使用
  • 启动主工程的时候,添加参数 -javaagent:agent jar包的路径=HELLOAGENT

  • 运行主程序,会首先打印出如下内容:

    premain 方法执行 1
    HELLOAGENT
    
  • 可以看到 java agent 的代码优先于 MAIN 函数的执行。

    10.4. 示例:统计方法的调用时间

    10.4.1. Byte Buddy
  • Skywalking 中对每个调用的时长都进行了统计,这一小节中我们会使用 ByteBuddy 和 Java agent 技术来统计方法的调用时长。

  • Byte Buddy 是开源的、基于 Apache 2.0 许可证的库,它致力于解决字节码操作和 instrumentation API 的复杂性。Byte Buddy 所声称的目标是将显式的字节码操作隐藏在一个类型安全的领域特定语言背后。通过使用 Byte Buddy,任何熟悉 Java 编程语言的人都有望非常容易地进行字节码操作。ByteBuddy 提供了额外的 API 来生成 Java agent,可以轻松的增强我们已有的代码。
    10.4.2. 添加依赖
    <dependency>
      <groupId>net.bytebuddy</groupId>
      <artifactId>byte-buddy</artifactId>
      <version>1.10.14</version>
    </dependency>
    <dependency>
      <groupId>net.bytebuddy</groupId>
      <artifactId>byte-buddy-agent</artifactId>
      <version>1.10.14</version>
      <scope>test</scope>
    </dependency>
    

10.4.3. PreMainAgent 代码
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;

import java.lang.instrument.Instrumentation;

public class PreMainAgent {

    public static void premain(String agentArgs, Instrumentation inst) {
        //创建一个转换器,转换器可以修改类的实现
        //ByteBuddy对java agent提供了转换器的实现,直接使用即可 AgentBuilder.Transformer transformer = new AgentBuil
        AgentBuilder.Transformer transformer = new AgentBuilder.Transformer() {
            @Override
            public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule) {
                return builder
                        // 拦截任意方法
                        .method(ElementMatchers.<MethodDescription>any())
                        // 拦截到的方法委托给 TimeInterceptor
                        .intercept(MethodDelegation.to(MyInterceptor.class));
            }
        };

        new AgentBuilder // Byte Buddy 专门有个 AgentBuilder 来处理 Java Agent 场景
                .Default()
                // 根据包名前缀拦截类
                .type(ElementMatchers.nameStartsWith("com.myspace.server"))
                // 拦截到的类由 transformer 处理
                .transform(transformer)
                .installOn(inst);
    }

}
  • 先生成一个转换器,ByteBuddy 提供了java agent 专用的转换器。通过实现 Transformer 接口利用 builder 对象来创建一个转换器。转换器可以配置拦截方法的格式,比如用名称,本例中拦截所有方法,并定义一个拦截器类 MyInterceptor。
  • 创建完拦截器之后可以通过 Byte Buddy 的 AgentBuilder 建造者来构建一个 agent 对象。AgentBuilder 可以对指定的包名前缀来生效,同时需要指定转换器对象。
    10.4.4. MyInterceptor
    ```java import net.bytebuddy.implementation.bind.annotation.Origin; import net.bytebuddy.implementation.bind.annotation.RuntimeType; import net.bytebuddy.implementation.bind.annotation.SuperCall;

import java.lang.reflect.Method; import java.util.concurrent.Callable;

public class MyInterceptor {

@RuntimeType
public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable) throws Exception {
    long start = System.currentTimeMillis();
    try {
        // 执行原方法
        return callable.call();
    } finally {
        // 打印调用时长
        System.out.println(method.getName() + ":" + (System.currentTimeMillis() - start) + "ms");
    }
}

}


- MyInterceptor 就是一个拦截器的实现,统计的调用的时长。参数中的 method 是反射出的方法对象,而 callable 就是调用对象,可以通过 callable.call() 方法来执行原方法。
<a name="6685e333"></a>
##### 10.4.5. 重新打包
<a name="cb76820a"></a>
##### 10.4.6. 修改主程序代码,将 Main 类放到 `com.myspace.server` 包下。

```java
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(1000);
        System.out.println("Hello World!");
    }
}

10.4.7. 运行这个 Main 类
  • 结果如下: ``` Hello World! main:1001ms

Process finished with exit code 0 ```