1.APM客户采集典型的三种方案
Pinpoint:基本不用修改源码和配置文件,只要在启动命令里指定javaagent参数即可,对于运维人员来讲最为方便;
Zipkin:需要对Spring、web.xml之类的配置文件做修改,相对麻烦一些;
CAT:因为需要修改源码设置埋点,因此基本不太可能由运维人员单独完成,而必须由开发人员的深度参与了,而很多开发人员是比较抗拒在代码中加入这些东西滴;
2.使用-javaagent 加载的包,多classloader理解很关键。
2.1.javaagent加载器是什么
An agent is just an interceptor in front of your main method, executed in the same JVM and loaded by the same system classloader, and governed by the same security policy and context.
4. One java application may have any number of agents by using -javaagent:
option any number of times. Agents are invoked in the same order as specified in options.
http://javahowto.blogspot.nl/2006/07/javaagent-option.html
http://stackoverflow.com/questions/26280406/how-to-specify-class-path-for-java-agent
2.2.-javaagent 加载jar
-javaagent加载的jar包,在AppClassLoader。
但是代码里可以把一些包加到bootstrap classloader
Instrumentation.appendToBootstrapClassLoaderSearch
添加到AppClassLoader用:
Instrumentation.appendToSystemClassLoaderSearch
2.3.classloader 开发调试和容器启动区别
用IDE 启动classloader:
sun.misc.Launcher$AppClassLoader@18b4aac2
->
sun.misc.Launcher$ExtClassLoader@27f674d
->
bootstrap classloader.
web服务器启动
比如jetty启动web应用,应用的classloader是WebAppClassLoader。
WebAppClassLoader=170949260@a307a8c -> startJarLoader@4d1c00d0 -> sun.misc.Launcher$AppClassLoader@18b4aac2 -> sun.misc.Launcher$ExtClassLoader@63947c6b-> bootstrap classloader.
3.探针javaagent 方式考虑的主要问题
java agent功能涉及到的第三方包和应用里的包冲突问题。
一般javaagent要实现的功能有:
- 在框架的采集点进行字节码修改
- 采集到数据进行上报
4.一般的实现方法
4.1.把第三方包重命名来避免冲突
gradle:jarjar
apply plugin: 'org.anarres.jarjar'
compile jarjar.repackage {
from "org.slf4j:slf4j-api:${vLogSlf4j}"
//classDelete "org.slf4j.**"
classRename "org.slf4j.**","com.xxx.org.slf4j.@1"
setDestinationName "slf4j-api-${vLogSlf4j}-jarjar.jar"
}
maven shade重命名:
4.2 通过自定义classloder加载,实现区分。
比如:采集涉及到的上报部分,一般会有很多公共包,上报的相关包就可以通过自定classloader加载。
目前我考虑的一个agent的目录结构及加载方式:
gtrace
+all ------打包和release打包复制(包括agent,core,plugins)
+agent------做javaagent启动和类加载相关
+core ------核心实现(比如opentracing的api,reporter的接口)
+api ------ core功能API,可以通过反射实现,应用如果有定制开发可以用,发布到仓库
+reporter ---采集数据上报具体实现(有可能是kafka,http等)
+plugins ----各个框架的探针相关代码(依赖的包一般是provided,不会出依赖包)
+--plugin-dubbo
+--plugin-hessian
类加载策略:
agent+core及依赖包,把第三方包重命名打包(可以合并打包),加载到systemClassloader
plugins的相关包,加载到跟应用加载一样的classloader,可以通过反射调用UrlClassloader.addUrl动态添加
reporter包,可以通过自定定义classloader加载,然后通过serviceloader加载奥实现,给agent调用。
5.其他一些问题
5.1. manifest文件设置
premain-class 一般不会忘,但是下面两个属性可能会忘,报错
java.lang.UnsupportedOperationException: adding retransformable transformers is not supported in this environment
打包的时候,设置这个2个属性
manifest.attributes["Can-Retransform-Classes"]=true manifest.attributes["Can-Redefine-Classes"]=true
5.2 大包结果确认,最好准备一个反编译工具
mac我用:JD-GUI
5.3 javassist
getDeclaredMethods 才能修改,getMethod不能修改,自己没留意坑了不少时间,想想也是只能本类声明的方法
参考:
用 Javassist 进行类转换:https://www.ibm.com/developerworks/cn/java/j-dyn0916/
API:https://jboss-javassist.github.io/javassist/tutorial/tutorial2.html
5.4 打包可能会包多个包打到一起或者copy
有些可能比较累赘,没有细究,maven例子如下:
<build>
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<executions>
<execution>
<id>clean-first</id>
<phase>generate-resources</phase>
<goals>
<goal>clean</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<!-- Populate the properties whose key is groupId:artifactId:type
and whose value is the path to the artifact -->
<execution>
<id>locate-dependencies</id>
<phase>initialize</phase>
<goals>
<goal>properties</goal>
</goals>
</execution>
<!-- Unpack all source files -->
<execution>
<id>unpack-sources</id>
<phase>prepare-package</phase>
<goals>
<goal>unpack-dependencies</goal>
</goals>
<configuration>
<classifier>sources</classifier>
<includes>com/xxx/gtrace/**</includes>
<includeScope>runtime</includeScope>
<includeGroupIds>${project.groupId}</includeGroupIds>
<outputDirectory>${generatedSourceDir}</outputDirectory>
</configuration>
</execution>
<!-- Unpack all class files -->
<execution>
<id>unpack-jars</id>
<phase>prepare-package</phase>
<goals>
<goal>unpack-dependencies</goal>
</goals>
<configuration>
<excludes>com/xxx/examples/**,com/xxx/gtrace/opentracing/plugin/**,com/xxx/gtrace/opentracing/brave/**</excludes>
<includes>com/xxx/**,META-INF/**,gtrace*</includes>
<includeScope>runtime</includeScope>
<includeGroupIds>${project.groupId}</includeGroupIds>
<outputDirectory>${project.build.outputDirectory}</outputDirectory>
</configuration>
</execution>
<!--copy jar-->
<execution>
<id>copy</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<includeGroupIds>io.opentracing</includeGroupIds>
<excludeGroupIds>io.opentracing.brave</excludeGroupIds>
<excludeScope>
provided
</excludeScope>
<outputDirectory>
${project.build.directory}/release
</outputDirectory>
</configuration>
</execution>
<execution>
<id>copy-tracer</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<excludeGroupIds>
com.xxx.gtrace
</excludeGroupIds>
<excludeScope>
provided
</excludeScope>
<excludeArtifactIds>opentracing-api,opentracing-noop,opentracing-util</excludeArtifactIds>
<outputDirectory>
${project.build.directory}/release/tracer
</outputDirectory>
</configuration>
</execution>
<execution>
<id>copy-plugin</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<includeArtifactIds>
gtrace-opentracing-plugin-hessian,gtrace-opentracing-plugin-dubbo,gtrace-opentracing-plugin-httpclient4,
gtrace-opentracing-plugin-servlet3,gtrace-opentracing-plugin-servlet25,gtrace-opentracing-plugin-springmvc
</includeArtifactIds>
<outputDirectory>
${project.build.directory}/release/plugin
</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<!-- Instead of generating a new version property file, merge others' version property files into one.
<execution>
<id>write-version-properties</id>
<phase>none</phase>
</execution>
<execution>
<id>merge-version-properties</id>
<phase>prepare-package</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<taskdef resource="net/sf/antcontrib/antlib.xml" />
<propertyselector property="versions" match="^(${project.groupId}:(?!netty-example)[^:]+:jar(?::[^:]+)?)$" select="\1" />
<for list="${versions}" param="x">
<sequential>
<unzip src="${@{x}}" dest="${dependencyVersionsDir}">
<patternset>
<include name="META-INF/${project.groupId}.versions.properties" />
</patternset>
</unzip>
<concat destfile="${project.build.outputDirectory}/META-INF/${project.groupId}.versions.properties" append="true">
<path path="${dependencyVersionsDir}/META-INF/${project.groupId}.versions.properties" />
</concat>
</sequential>
</for>
<delete dir="${dependencyVersionsDir}" quiet="true" />
</target>
</configuration>
</execution>
-->
<!-- Clean everything once finished so that IDE doesn't find the unpacked files. -->
<execution>
<id>clean-source-directory</id>
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<delete dir="${generatedSourceDir}" quiet="true"/>
<!--<delete dir="${dependencyVersionsDir}" quiet="true" />-->
<delete dir="${project.build.outputDirectory}" quiet="true"/>
</target>
</configuration>
</execution>
</executions>
</plugin>
<!-- Include the directory where the source files were unpacked -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-source</id>
<phase>prepare-package</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${generatedSourceDir}</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<!-- Disable OSGi bundle manifest generation -->
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<executions>
<execution>
<id>generate-manifest</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
<!-- Override the default JAR configuration -->
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
<id>default-jar</id>
<phase>none</phase>
</execution>
<execution>
<id>all-in-one-jar</id>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
<index>true</index>
<manifestEntries>
<Premain-Class>
com.xxx.gtrace.opentracing.agent.GtraceAgent
</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Pinpoint-Version>${project.version}</Pinpoint-Version>
</manifestEntries>
</archive>
<outputDirectory>${project.build.directory}/release</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<!-- Disable animal sniffer -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>animal-sniffer-maven-plugin</artifactId>
<executions>
<execution>
<id>default</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
<!-- Disable checkstyle -->
<plugin>
<artifactId>maven-checkstyle-plugin</artifactId>
<executions>
<execution>
<id>check-style</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
<!-- Disable all plugin executions configured by jar packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>default-resources</id>
<phase>none</phase>
</execution>
<execution>
<id>default-testResources</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>default-compile</id>
<phase>none</phase>
</execution>
<execution>
<id>default-testCompile</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<executions>
<execution>
<id>default-test</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
5.5 修改字节码➕拦截的实现思路(javassist)
在做APM的时候想简单实现下AOP,对plugin的拦截记录下思路,(pinpoint有一种较好的实现,想对麻烦点,要求学习字节码的一些知识,主要看InstrumentClass实现,JavassistClass和ASMClass )
1.通过子类方式(这类方式,有相对统一的对象创建的地方)
http://exolution.iteye.com/blog/1460833
动态创建一个被代理类的子类,子类里实现拦截。
2.通过创建父亲类的方式(这种方式,直接修改字节码)
代码写一个需要修改的代理抽象类,保持父类、实现接口类跟目标一致。
public abstract class HXXXProxy implements InvocationHandler, Serializable { //目标修改类的方法,重命名成一致的名字 public abstract Object invoke$impl(Object proxy, Method method, Object[] args) throws Throwable; public Object invoke$trace(final Object proxy, final Method method, final Object[] args) throws Throwable { //xxxxxx //调用原方法 Object reponse = invoke$impl(proxy, method, args);
//xxxxx return reponse;
}
}
修改字节码的时候,把目标类的父类,设置成代理类,目标方法重命名invoke$impl。
copy一个目标方法,把方法体改成 { return methodName$trace($$);}。