本人阅读了 Skywalking 的大部分核心代码,也了解了相关的文献,对此深有感悟,特此借助巨人的思想自己手动用JAVA语言实现了一个 "调用链监控APM" 系统。本书采用边讲解实现原理边编写代码的方式,看本书时一定要跟着敲代码。
作者已经将过程写成一部书籍,奈何没有钱发表,如果您知道渠道可以联系本人。一定重谢。
本书涉及到的核心技术与思想
JavaAgent , ByteBuddy,SPI服务,类加载器的命名空间,增强JDK类,kafka,插件思想,切面,链路栈等等。实际上远不止这么多,差不多贯通了整个java体系。
适用人群
自己公司要实现自己的调用链的;写架构的;深入java编程的;阅读Skywalking源码的;
版权
本书是作者呕心沥血亲自编写的代码,不经同意切勿拿出去商用,否则会追究其责任。
书籍目录和原版PDF+源码请见:
PDF书籍《手写调用链监控APM系统-Java版》第1章 开篇介绍-CSDN博客
第2章 配置文件系统的建立
配置文件对任何一个系统来说都是必不可少的,分为静态配置和动态配置两种。动态配置典型的就是nacos的配置中心,可以做到在服务端配置中心修改一个配置项,然后各个客户端微服务都会感知,然后自动修改各自jvm进程里面的配置。 当然这需要进行网络交互,我们重点不在于此。
我们就采用静态配置方式,设计一个本地的配置文件,项目启动时就会加载文件,然后读取解析文件,将值设置到jvm进程的程序中。配置文件值修改了,需要重启JVM。
本书设计方案是扫描项目环境下 hadluo-agent.config 文件,此文件类似属性文件,格式如下:
# kafka集群的地址
Bootstrap.servers = 127.0.0.1
# https://zhida.zhihu.com/search?content_id=251890693&content_type=Article&match_order=3&q=kafka&zhida_source=entity的topic
Bootstrap.topic = topic_apm
# 服务名称
#Agent.serviceName =
# 实例名
#Agent.serviceInstance =
等号前面的对应的是模块配置部分,后面的是值。
模块配置又分为两块,点号前面的是哪个模块的配置,点号后面的是模块的具体配置项。
我们将上面的配置文件进行抽象,可以得到对应的配置类:
public class Config {
public static class Agent {
public static String serviceName ;
public static String serviceInstance ;
}
public static class Bootstrap {
public static String servers ="127.0.0.1";
public static String topic = "hadluo-apm-topic";
}
}
上面配置类指定了2个模块,Agent和Bootstrap,Agent模块又包含serviceName和serviceInstance配置项。
2.1 配置文件的加载
我们开始实现配置代码,首先在apm-commons项目下新建类:
com.hadluo.apm.commons.Config
public class Config {
public static class Agent {
//agent的服务名称
public static String serviceName ;
// 实例名
public static String serviceInstance ;
}
public static class Bootstrap {
// 后端OAP kafka地址
public static String servers;
// kafka topic
public static String topic;
}
}
暂时我们先想到的是这几个配置,后续我们会不断增加。
然后需要实现读取解析配置文件并映射到Config类的逻辑,在apm-agent-core模块里面新建类:
com.hadluo.apm.agentcore.config.SnifferConfigInitializer
public class SnifferConfigInitializer {
public static void initializeCoreConfig(String agentOptions) throws Exception {
// 设置 应用名
injectConfig("Agent.serviceName",agentOptions) ;
// 设置 serviceInstance
injectConfig("Agent.serviceInstance", UUID.randomUUID().toString().replaceAll("-", "") + "@" + OSUtil.getIPV4()) ;
// 读取环境下面的 hadluo-agent.config 文件
File confFile = ResourceFinder.findFile("hadluo-agent.config").get(0);
Files.lines(Paths.get(confFile.getAbsolutePath())).forEach(line -> {
// 遍历每一行文件内容
line = line.trim();
// 如果是 # 开头的 都是 注释
if(line.startsWith("#") || line.isEmpty()){
return ;
}
if(line.contains("#")){
// 后面有注释需要截取掉
line = line.substring(0,line.indexOf("#")) ;
}
if(!line.contains("=") || !line.contains(".")){
// 没有 = 和 . 都是不合法的
return ;
}
try {
// 反射注入到装载类的静态字段里面
injectConfig(line.split("=")[0].trim(),line.split("=")[1].trim()) ;
} catch (Exception e) {
Logs.err(SnifferConfigInitializer.class , "配置文件错误" , e);
}
}) ;
}
private static void injectConfig (String key , String value) throws Exception {
String[] splits = key.split("\\.");
// 获取模块类
Class<?> moduelClass = Class.forName(Config.class.getName() + "$" + splits[0]) ;
// 获取静态字段
Field field = moduelClass.getField(splits[1]);
// 设置值
field.set(null , value);
}
}
说明: ResourceFinder和OSUtil都是工具类。
以上代码不难,injectConfig 就是通过反射Config静态字段将值设置到字段上面。注意**内部类是用进行分隔的。**比如:com.hadluo.apm.commons.ConfigBootstrap
initializeCoreConfig逻辑先inject了serviceName和serviceInstance, 然后去解析配置文件,将每个配置都进行inject, 解析到#就是注释,这也符合properties文件的格式。这样如果配置文件设置了serviceName和serviceInstance也就优先配置文件的。
2.2 配置文件的测试
在premain方法中,添加下面代码进行调用和测试:
// 1. 初始化配置
try {
SnifferConfigInitializer.initializeCoreConfig(args);
} catch (Exception e) {
Logs.err(AgentMain.class , "初始化配置失败" , e);
return ;
}
// 测试打印,后续要去掉
System.out.println("servers="+Config.Bootstrap.servers);
System.out.println("topic="+Config.Bootstrap.topic);
System.out.println("serviceInstance="+Config.Agent.serviceInstance);
System.out.println("serviceName="+Config.Agent.serviceName);
新建 hadluo-agent.config 配置文件放到resource目录下:
修改amp-agent-core的代码后,需要重新进行 package编译,生成jar,然后启动测试SpringBoot项目会打印出:
发现我们的配置值已经成功都写到了Config装载类中了。serviceInstance也已经成功注入,这些都为以后链路数据奠定了雄厚的基础。
serviceName的值是取得启动参数里面的,我们没有配置所以为空,理论上这个应该是自动获取到微服务的名称然后设置,获取微服务的名称需要用到插桩插件,我们后续讲解。
2.3 本章小结
本章主要讲解了如何设计一个简单的配置文件,并编写出了核心类SnifferConfigInitializer 去加载解压配置,相对较简单,真实的apm设计的配置是通过AOP后端可以动态修改的,类似与微服务里面的注册中心,由于配置文件不是本书的重点,所以这样简单设计了。
本章涉及的工具代码
com.hadluo.apm.commons.OSUtil
public class OSUtil {
private static volatile String OS_NAME;
private static volatile String HOST_NAME;
private static volatile List<String> IPV4_LIST;
private static volatile int PROCESS_NO = 0;
public static String getOsName() {
if (OS_NAME == null) {
OS_NAME = System.getProperty("os.name");
}
return OS_NAME;
}
public static String getHostName() {
if (HOST_NAME == null) {
try {
InetAddress host = InetAddress.getLocalHost();
HOST_NAME = host.getHostName();
} catch (UnknownHostException e) {
HOST_NAME = "unknown";
}
}
return HOST_NAME;
}
public static List<String> getAllIPV4() {
if (IPV4_LIST == null) {
IPV4_LIST = new LinkedList<>();
try {
Enumeration<NetworkInterface> interfs = NetworkInterface.getNetworkInterfaces();
while (interfs.hasMoreElements()) {
NetworkInterface networkInterface = interfs.nextElement();
Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
while (inetAddresses.hasMoreElements()) {
InetAddress address = inetAddresses.nextElement();
if (address instanceof Inet4Address) {
String addressStr = address.getHostAddress();
if ("127.0.0.1".equals(addressStr)) {
continue;
} else if ("localhost".equals(addressStr)) {
continue;
}
IPV4_LIST.add(addressStr);
}
}
}
} catch (SocketException e) {
}
}
return IPV4_LIST;
}
public static String getIPV4() {
final List<String> allIPV4 = getAllIPV4();
if (allIPV4.size() > 0) {
return allIPV4.get(0);
} else {
return "no-hostname";
}
}
public static int getProcessNo() {
if (PROCESS_NO == 0) {
try {
PROCESS_NO = Integer.parseInt(ManagementFactory.getRuntimeMXBean().getName().split("@")[0]);
} catch (Exception e) {
PROCESS_NO = -1;
}
}
return PROCESS_NO;
}
}
com.hadluo.apm.commons.ResourceFinder
public class ResourceFinder {
/***
* 从所有环境中搜索文件,包括第三方jar
* @param fileName
* @return
* @throws IOException
*/
public static List<File> findFile(String fileName) throws IOException {
// 所有classpath环境加载
String DEFAULT_RESOURCE_PATTERN = "**/*.";
String endPrifix = fileName.substring(fileName.lastIndexOf(".") + 1);
DEFAULT_RESOURCE_PATTERN = DEFAULT_RESOURCE_PATTERN + endPrifix;
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
+ org.springframework.util.ClassUtils.convertClassNameToResourcePath(
SystemPropertyUtils.resolvePlaceholders(""))
+ "/" + DEFAULT_RESOURCE_PATTERN;
Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);
List<File> files = new ArrayList<File>();
for (Resource resource : resources) {
if (resource.getFilename().trim().equals(fileName.trim())) {
try {
files.add(resource.getFile());
} catch (Exception e) {
}
// 有可能是在jar里面
try {
files.add(parserJar(resource, fileName));
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
}
return files;
}
private static File parserJar(Resource resource, String fileName) throws IOException, URISyntaxException {
// 在linux spring打包后运行:/opt/neighbour-business-friends.jar!/BOOT-INF/lib/neighbour-agent-elasticsearch-starter-0.0.19.jar!/agent-client-0.0.1-SNAPSHOT-jar-with-dependencies.jar
// 本地运行 : /F:/hadluo/code_src/hadluo-smart-apm/hadluo-smartapm-starter/target/classes/apm-agent-core-1.0-jar-with-dependencies.jar!/hadluo-apm/hadluo-apm-plugin.def
// 区别就在于spring 会把运行的应该达成jar,多了这一层
String jarPath = resource.toString().replace("URL [jar:file:", "").replace("]", "").trim();
String[] fileItems = jarPath.split("!");
String copyTempDir = null;
String lastLevelJar = null;
if (fileItems.length == 1) {
if (new File(fileItems[0]).getName().equals(fileName)) {
return new File(fileItems[0]);
}
}
for (String fileItem : fileItems) {
if (copyTempDir != null) {
// 从 上一层的jar中 拷贝出 当前的文件 到 copyTempDir
File currentFile = loadRecourseFromJarByFolder(lastLevelJar, copyTempDir, getName(fileItem));
if (fileName.equals(currentFile.getName())) {
// 如果当前的文件就是我们要提取的 文件,直接返回了
return currentFile;
}
// 还要继续下一层 解析
copyTempDir = currentFile.getParent();
lastLevelJar = currentFile.getAbsolutePath();
}
if (fileItem.endsWith(".jar")) {
// 需要从jar中拷贝出来
copyTempDir = new File(fileItem).getParent();
lastLevelJar = fileItem;
}
}
return null;
}
private static String getName(String path) {
return new File(path).getName();
}
/**
* 提取jar包文件夹到指定文件
*
* @throws IOException
*/
private static File loadRecourseFromJarByFolder(String jarFilePath, String destinationDirectory, String filter)
throws IOException {
// String jarFilePath = "path/to/your.jar"; // JAR文件的路径
// String destinationDirectory = "path/to/destination/directory"; // 目标文件夹的路径
FileInputStream fis = new FileInputStream(jarFilePath);
BufferedInputStream bis = new BufferedInputStream(fis);
JarInputStream jis = new JarInputStream(bis);
try {
JarEntry entry;
while ((entry = jis.getNextJarEntry()) != null) {
if (!entry.isDirectory()) {
String fileName = entry.getName();
if (!fileName.contains(filter)) {
continue;
}
File outputFile = new File(destinationDirectory, fileName);
File parentDir = outputFile.getParentFile();
if (parentDir != null && !parentDir.exists()) {
parentDir.mkdirs();
}
FileOutputStream fos = new FileOutputStream(outputFile);
BufferedOutputStream bos = new BufferedOutputStream(fos);
try{
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = jis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
}
return outputFile;
}finally {
bos.close();
fos.close();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}