在系统性能监控方法上,Skywalking 提出了代码级性能剖析这种在线诊断方法。这种方法基于一个高级语言编程模型共性,即使再复杂的系统,再复杂的业务逻辑,都是基于线程去进行执行的,而且多数逻辑是在单个线程状态下执行的;代码级性能剖析就是利用方法栈快照,并对方法执行情况进行分析和汇总;并结合有限的分布式追踪 span 上下文,对代码执行速度进行估算。有如下优势:
SkyWalking的跟踪或者说性能剖析,选择某个服务
根据选择端点的名称及相应的规则建立任务,后续再调用任务列表的端口会自动记录剖析剖析当前端口数据并生成剖析结果
为了更好演示在库存微服务的创建订单方法中增加一个睡眠3秒,然后重新启动订单微服务
再次多次访问创建订单接口 http://localhost:4070/order/create/1000/1001/2 ,需要连续执行多次请求,因为存在采样设置。如果执行次数少,可能不会出现采样数据,每个服务,相同时间只能添加一个任务,添加的任务不能更改,也不能删除,只能等待过期后自动删除。
在库存和订单微服务中引入依赖
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-logback-1.x</artifactId>
<version>8.11.0</version>
</dependency>
在库存和订单微服务中,增加分布式链路追踪ID在logback.xml加入如下配置,[%tid]
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%tid] [%thread] %-5level %logger{36} -%msg%n</Pattern>
</layout>
</encoder>
</appender>
gRPC reporter上报日志在logback.xml加入如下配置:
<appender name="grpc-log" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.mdc.TraceIdMDCPatternLogbackLayout">
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{tid}] [%thread] %-5level %logger{36} -%msg%n</Pattern>
</layout>
</encoder>
</appender>
访问订单接口 http://localhost:4070/order/create/1000/1001/2,查看订单和库存微服务的日志中已带有 TID
也通过GRPC上传到SkyWalking后端,通过Log页面可以查看日志信息
可以通过TID查询对应日志详细信息
在config/alarm-settings.yml ,已经默认若干项告警,我们简单修改告警信息内容,增加一串标识"Itxs Alarm"例如,配置webhooks
rules:
# Rule unique name, must be ended with `_rule`.
service_instance_resp_time_rule:
metrics-name: service_instance_resp_time
op: ">"
threshold: 1000
period: 10
count: 2
silence-period: 5
message: Itxs Alarm esponse time of service instance {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: Itxs Alarm esponse time of endpoint relation {name} is more than 1000ms in 2 minutes of last 10 minutes
webhooks:
- http://192.168.4.210:8080/alarm/
新建一个webhooks接口服务端,创建AlarmMessage实体类
package com.aotain.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AlarmMessage implements Serializable {
private String scopeId;
private String scope;
private String name;
private String id0;
private String id1;
private String ruleName;
//告警的消息
private String alarmMessage;
//告警的产生时间
private Long startTime;
}
创建一个控制器AlarmController,提供/alarm接口,这里简单就显示信息,后续可以根据实际调用微信、钉钉告警之类。
package com.aotain.controller;
import com.aotain.entity.AlarmMessage;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class AlarmController {
@PostMapping("/alarm")
public String alarm(@RequestBody List<AlarmMessage> alarmMessageList) throws Exception {
System.out.println(alarmMessageList);
return "ok";
}
}
这里使用ApiFox多线程访问http://localhost:4070/order/create/1000/1001/2
查看告警,已经为我们修改后告警信息,含有Itxs Alarm的前缀字符串
查看事件
查看webhooks调用接口,已经收到SkyWalking调用过来的数据,这里后续可扩展为实际的告警方式处理。
上面使用Skywalking并没有修改程序中任何一行 Java 代码,这里便是使用到了 Java Agent 技术,如果平常基于增删改查业务逻辑那就基本不会使用到Java Agent,但我们平时用过的不少工具如热部署工具JRebel,SpringBoot的热部署插件,各种线上诊断工具(btrace, greys),阿里开源的arthas都是基于java Agent来实现的。在JDK1.5以后就有java Agent,使用agent技术构建一个独立于应用程序的代理程序(即Agent),用来协助监测、运行甚至替换其他JVM上的程序。使用它可以实现虚拟机级别的AOP功能,典型的优势就是无代码侵入。Agent大体可分为两种:
premain为主程序之前运行的Agent,在实际使用过程中,javaagent是java命令的一个参数。通过java 命令启动我们的应用程序的时候,可通过参数 -javaagent 指定一个 jar 包(也就是我们的代理agent),能够实现在我们应用程序的主程序运行之前来执行我们指定jar包中的特定方法,在该方法中我们能够实现动态增强Class等相关功能,并且该 jar包有2个要求:
从字面上理解,Premain-Class 就是运行在 main 函数之前的的类。当Java 虚拟机启动时,在执行 main 函数之前,JVM 会先运行-javaagent
所指定 jar 包内 Premain-Class 这个类的 premain 方法 。
-javaagent
所在包java.lang.instrument
,是rt.jar
中定义的一个包,有两个重要的类:
java.lang.instrument包提供了一些工具帮助开发人员在 Java 程序运行时动态修改系统中的 Class 类型。其中使用该软件包的一个关键组件就是 Javaagent,从本质上来讲,Java Agent 是一个遵循一组严格约定的常规 Java 类,就如上面说到 javaagent命令要求指定的类中必须要有premain()方法,并且对premain方法的签名也有要求,签名必须满足以下两种格式:
public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)
JVM 会优先加载 带 Instrumentation
签名的方法,加载成功忽略第二种,如果第一种没有,则加载第二种方法。
创建PreAgentDemo的maven项目,编写一个agent程序com.itxs.agent.PreAgentDemo,完成premain
方法的签名,这里先做一个简单的日志输出。
package com.itxs.agent;
import java.lang.instrument.Instrumentation;
public class PreAgentDemo {
public static void premain(String agentArgs, Instrumentation instrumentation) {
System.out.println("PreAgentDemo run");
System.out.println("PreAgentDemo receive params agentArgs=" + agentArgs);
}
}
maven项目pom文件增加如下坐标
<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>
<!-- 添加 mplementation-*和Specification-*配置项-->
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
<addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
</manifest>
<manifestEntries>
<!--指定premain方法所在的类-->
<Premain-Class>com.itxs.agent.PreAgentDemo</Premain-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>
对PreAgentDemo
项目进行打包,得到 PreAgentDemo-1.0.jar
,放在G:\other下,查看jar包中的MANIFEST.MF文件
Can-Redefine-Classes :true表示能重定义此代理所需的类,默认值为 false(可选)
Can-Retransform-Classes :true 表示能重转换此代理所需的类,默认值为 false (可选)
Premain-Class :包含 premain 方法的类(类的全路径名)
接着创建一个test-demo项目,编写一个简单测试类App,运行JVM参数添加
-javaagent:G:\other\PreAgentDemo-1.0.jar=param1=value1,param2=value2,param3=value3
上运行结果可以看到在测试程序main函数启动前先输出premain方法打印的日志。实际开发中大部分类加载都会通过该方法。当然,遗漏的主要是系统类,因为很多系统类先于 agent 执行,而用户类的加载肯定是会被拦截的。也就是说,这个方法是在 main 方法启动前拦截大部分类的加载活动,既然可以拦截类的加载,那么就可以去做重写类这样的操作,结合第三方的字节码编译工具,比如ASM,bytebuddy,javassist,cglib等等来改写实现类。
Instrumentation 中的核心 API 方法:
agentmain,可以在 main 函数开始运行之后再运行。跟premain
函数一样, 开发者可以编写一个含有agentmain
函数的 Java 类。
public static void agentmain (String agentArgs, Instrumentation inst)
public static void agentmain (String agentArgs)
同样需要在MANIFEST.MF文件里面设置“Agent-Class”来指定包含 agentmain 函数的类的全路径。在前面工程基础上增加com.itxs.agent.AgentDemo文件,也是简单打印日志。
package com.itxs.agent;
import java.lang.instrument.Instrumentation;
public class AgentDemo {
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
System.out.println("AgentDemo run");
}
}
在pom.xml中添加配置如下
<Agent-Class>com.itxs.agent.AgentDemo</Agent-Class>
重新打包 PreAgentDemo-1.0.jar
并覆盖到G:\other下,在测试类App修改如下代码
package com.itxs;
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
public class App
{
public static void main( String[] args ) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
System.out.println( "itxs app main run!" );
//获取当前系统中所有 运行中的 虚拟机
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vm : list) {
if (vm.displayName().endsWith("com.itxs.App")) {
VirtualMachine virtualMachine = VirtualMachine.attach(vm.id());
virtualMachine.loadAgent("G:/other/PreAgentDemo-1.0.jar");
virtualMachine.detach();
}
}
}
}
list()方法会去寻找当前系统中所有运行着的JVM进程,你可以打印vmd.displayName()看到当前系统都有哪些JVM进程在运行。因为main函数执行起来的时候进程名为当前类名,所以通过这种方式可以去找到当前的进程id。在windows中安装的jdk无法找到,如遇到这种情况手动将你jdk安装目录下:lib目录中的tools.jar添加进当前工程的Libraries中。
agent要在主程序运行后加载,我们不可能在主程序中编写加载的代码,只能另写程序,那么另写程序如何与主程序进行通信?这里用到的机制就是attach机制,它可以将JVM A连接至JVM B,并发送指令给JVM B执行。
Byte Buddy是一个代码生成和操作库,用于在Java应用程序运行时创建和修改Java类,并且不需要编译器的帮助。与Java类库附带的代码生成实用程序不同,Byte Buddy允许创建任意类,并且不局限于为创建运行时代理实现接口。此外,Byte Buddy提供了一个方便的API,可以手动更改类,可以使用Java代理,也可以在构建期间更改类。
反射机制可以知道调用的方法或字段,但反射性能很差,反射能绕开类型安全检查,不安全,比如权限暴力破解;java编程语言代码生成库也有多种:
上面所有代码生成技术中推荐使用Byte Buddy,因为Byte Buddy代码生成可的性能最高;Byte Buddy 的主要侧重点在于生成更快速的代码,如下图
Class<?> dynamicType = new ByteBuddy()
// 生成 Object的子类
.subclass(Object.class)
// 生成类的名称
.name("com.itxs.type")
// 拦截其中的toString()方法
.method(ElementMatchers.named("toString"))
// 让toString()方法返回固定值
.intercept(FixedValue.value("Hello World!"))
.make()
// 加载新类型,默认WRAPPER策略,也即是ClassLoadingStrategy.Default.WRAPPER可以不写
.load(getClass().getClassLoader(),ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();
Byte Buddy 动态增强代码总有如下三种方式:
上面三种增强代码后得到的是 DynamicType.Unloaded 对象,表示的是一个未加载的类型,可以使用 ClassLoadingStrategy加载此类型;Byte Buddy
提供了几种类加载策略,这些加载策略定义在 ClassLoadingStrategy.Default
中:
method() 方法可以通过传入的 ElementMatchers 参数匹配多个需要修改的方法,这里的ElementMatchers.named("toString") 即为按照方法名匹配 toString() 方法。如果同时存在多个重载方法,则可以使用 ElementMatchers 其他 API 描述方法的签名,如下所示
// 指定方法名称
ElementMatchers.named("toString")
// 指定方法的返回值
.and(ElementMatchers.returns(String.class))
// 指定方法参数
.and(ElementMatchers.takesArguments(0));
intercept() 方法,通过 method()方法拦截到的所有方法会由 Intercept() 方法指定的 Implementation 对象决定如何增强;这里的 FixValue.value() 会将方法的实现修改为固定值,上例中就是固定返回 “Hello World!” 字符串。Byte Buddy 中可以设置多个 method() 和 Intercept() 方法进行拦截和修改,Byte Buddy 会按照栈的顺序来进行拦截。
在test-demo项目中添加ByteBuddy的依赖
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.12.12</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.12.12</version>
<scope>test</scope>
</dependency>
创建普通类OrderService
package com.itxs.service;
public class OrderService {
public String addOrder(){
System.out.println("=====do addOrder==========");
return "1000000001";
}
public String getOrder(String orderId){
System.out.println("=====do getOrder==========");
return orderId;
}
public String getOrder(String orderId,String status){
System.out.println("=====do getOrder two params==========");
return orderId+status;
}
}
创建拦截器类TestInterceptor
package com.itxs.interceptor;
import net.bytebuddy.implementation.bind.annotation.*;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
public class TestInterceptor {
@RuntimeType //将返回值转换成具体的方法返回值类型,加了这个注解 intercept 方法才会被执行
public Object intercept(
// 被拦截的目标对象 (动态生成的目标对象)
@This Object target,
// 正在执行的方法Method 对象(目标对象父类的Method)
@Origin Method method,
// 正在执行的方法的全部参数
@AllArguments Object[] argumengts,
// 目标对象的一个代理
@Super Object delegate,
// 方法的调用者对象 对原始方法的调用依靠它
@SuperCall Callable<?> callable) throws Exception {
//目标方法执行前执行日志记录
System.out.println("prepare do method="+method.getName());
// 调用目标方法
Object result = callable.call();
//目标方法执行后执行日志记录
System.out.println("have down method="+method.getName());
return result;
}
}
创建普通类代理测试类ByteBuddyTest
package com.itxs;
import com.itxs.interceptor.TestInterceptor;
import com.itxs.service.OrderService;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
public class ByteBuddyTest {
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
Class<? extends OrderService> generateClass = new ByteBuddy()
// 创建一个UserService 的子类
.subclass(OrderService.class)
//指定类的名称
.name("com.itxs.service.OrderServiceImpl")
// 指定要拦截的方法
.method(ElementMatchers.named("getOrder").and(ElementMatchers.returns(String.class).and(ElementMatchers.takesArguments(2))))
// 为方法添加拦截器 如果拦截器方法是静态的 这里可以传 LogInterceptor.class
.intercept(MethodDelegation.to(new TestInterceptor()))
// 动态创建对象,但还未加载
.make()
// 设置类加载器 并指定加载策略(默认WRAPPER)
.load(ByteBuddy.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
// 开始加载得到 Class
.getLoaded();
OrderService orderService = generateClass.newInstance();
System.out.println(orderService.addOrder());
System.out.println(orderService.getOrder("2000000000"));
System.out.println(orderService.getOrder("3000000000","支付中"));
}
}
在程序中用到ByteBuddy的MethodDelegation对象,它可以将拦截的目标方法委托给其他对象处理,注解使用说明如下:
运行ByteBuddyTest,增强的方法输出就是上面代码中方法匹配名称为getOrder且返回值为String且有两个入参的结果。
Java Agent十分强大,使用Transformer等高级功能进行类替换,方法修改等,要使用Instrumentation的相关API则需要对字节码等技术有较深的认识。接下来ByteBuddy结合Java Agent技术实现一个统计方法耗时的示例。
在上面的PreAgentDemo项目中加入依赖byte-buddy和byte-buddy-agent的依赖,上面测试工程Pom文件有
创建耗时统计拦截器类
package com.itxs.agent.interceptor;
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 TimeConsumingInterceptor {
/***
* 拦截方法
* @param method:拦截的方法
* @param callable:调用对象的代理对象
* @return
* @throws Exception
*/
@RuntimeType // 声明为static
public static Object intercept(@Origin Method method,
@SuperCall Callable<?> callable) throws Exception {
//时间统计开始
long start = System.currentTimeMillis();
// 执行原函数
Object result = callable.call();
//执行时间统计
System.out.println(method.getName() + ":time consuming total" + (System.currentTimeMillis() - start) + "ms");
return result;
}
}
创建JavaAgentCase的premain实现
package com.itxs.agent;
import com.itxs.agent.interceptor.TimeConsumingInterceptor;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
import java.lang.instrument.Instrumentation;
public class JavaAgentCase {
/***
* 执行方法拦截
* @param agentArgs:-javaagent 命令携带的参数。在前面介绍 SkyWalking Agent 接入时提到
* agent.service_name 这个配置项的默认值有三种覆盖方式,
* 其中,使用探针配置进行覆盖,探针配置的值就是通过该参数传入的。
* @param instrumentation:java.lang.instrumen.Instrumentation 是 Instrumention 包中定义的一个接口,它提供了操作类定义的相关方法。
*/
public static void premain(String agentArgs, Instrumentation instrumentation) {
// 动态构建操作,根据transformer规则执行拦截操作,匹配上的具体的类型描述
AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> {
// 构建拦截规则
return builder
// method()指定哪些方法需要被拦截,ElementMatchers.any()表示拦截所有方法
.method(ElementMatchers.any())
// intercept()指定拦截上述方法的拦截器
.intercept(MethodDelegation.to(TimeConsumingInterceptor.class));
};
// 采用Byte Buddy的AgentBuilder结合Java Agent处理程序
new AgentBuilder
// 采用ByteBuddy作为默认的Agent实例
.Default()
// 拦截匹配方式:类以com.itxs.service开始,也即是com.itxs.service包下的所有类
.type(ElementMatchers.nameStartsWith("com.itxs.service"))
// 拦截到的类由transformer处理
.transform(transformer)
// 安装到 Instrumentation
.installOn(instrumentation);
}
}
重新打包好PreAgentDemo-1.0.jar,准备测试类UserService.java
package com.itxs.service;
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class UserService {
private static Random random = new Random();
public void getUser(){
System.out.println("=====do getUser==========");
try {
TimeUnit.SECONDS.sleep(random.nextInt(5));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void updateUser(){
System.out.println("=====do updateUser==========");
try {
TimeUnit.SECONDS.sleep(random.nextInt(5));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
创建启动测试类
package com.itxs;
import com.itxs.service.UserService;
public class Application
{
public static void main( String[] args ) {
System.out.println("Application main start run-----------");
UserService service = new UserService();
service.getUser();
service.updateUser();
}
}
启动参数中jvm参数添加javaagent,可参考上面示例,执行Application的main后从日志可以看到UserService的方法被增强了
**本人博客网站 **IT小神 www.itxiaoshen.com
手机扫一扫
移动阅读更方便
你可能感兴趣的文章