先码个题目,慢慢补充开坑(萌新边记录边学习,欢迎大佬们互相讨论)
apache skywalking是当前主流最常用的开源apm系统之一,在已知的开源项目中系统性能,架构设计,功能丰富度,完善程度都可以算最优秀的一批,目前各公司针对此类观测系统的二开,定制化解决方案设计,私有化部署也十分关注,对观测/监控能力的需求也在提升,感觉这个领域的发展应当还是很有潜力的。
笔者曾经参与并负责过公司内部立项的全链路监控系统项目,包括整套apm系统的技术调研,需求统筹,部分子系统定制化二次开发,性能优化,容器化部署,上线保障等工作,该项目持续进行了一年并且进行了线上试点运行,目前项目仍在进行中。
经过一年的努力,对skywalking系统架构及apm相关技术也有了一定了解,在csdn上自己做一些笔记回顾自己的学习过程,同时也期待和各位大佬一同学习技术,文档内容有错的地方,欢迎各位大佬随时指正。
这次开坑主要先介绍的是skywalking java-agent探针的源码分析,本次先从agent探针的入口方法入手展开:
skywalking的java探针的核心功能是利用jdk1.5-1.6提供的instrumention机制进行实现的,选用的是探针前置加载(静态加载)的模式进行字节码增强,增强方法主函数入口为premain(1.6实现的JVM动态加载使用attach入口模式),具体实现方式为服务启动脚本命令(最简单的例如java -jar demo.jar)后添加启动项参数-javaagent:探针启动程序地址,例如:
-javaagent:D:\workspace\skywalking-agent\skywalking-agent.jar -Dskywalking.agent.service_name=sw::java_agent -Dskywalking.agent.instance_name=Agent_Main -Dskywalking.collector.backend_service=127.0.0.1:11800
当被监控服务启动时,探针程序优先于服务程序被加载,探针启动前置premain方法位于apm-sniffer模块中apm-agent目录下的SkyWalkingAgent类中:
这里面每一个关键步骤使用一个try catch进行异常处理,保证各个模块之间相互影响降到最低,同时不会对被监控服务的性能/运行造成影响:
上图中为探针程序配置加载逻辑,通过静态方法SnifferConfigInitializer.initializeCoreConfig()对探针外部配置进行读取,配置初始化操作:
这里读取的外部配置来源可以是探针的配置文件,jvm环境变量,服务命令的启动项参数
其中配置生效优先级顺序为:
启动项参数 > 环境变量 > 配置文件值
启动项参数就是在-javaagent后面添加的选项,格式类似于:
-javaagent:/path/to/skywalking-agent.jar=[option1]=[value1],[option2]=[value2]
option对应的就是配置项的key,具体格式见工程目录下的agent.config配置文件;value就是该项配置的值
环境变量有两种读取方式,一种是从启动项中读取,格式:-Dskywalking.option=vlue
一种是从JVM的设置的环境变量中读取value,ARG的变量名在agent.config中可以找到,例如
红框中的部分就是该配置项的环境变量名
这两种读取方式优先级:启动项环境变量 > 系统环境变量
配置文件agent.config中存储探针程序的默认配置,在程序启动时优先读取该文件作为初始化配置,之后先后读取环境变量配置和启动项配置对有变化的初始配置项进行覆盖。
1.配置加载方法源码解读
配置的初始化逻辑在premain方法中如下图:
使用的是原生封装的初始器SnifferConfigInitializer的静态方法initializeCoreConfig:
public static void initializeCoreConfig(String agentOptions) {
AGENT_SETTINGS = new Properties();
try (final InputStreamReader configFileStream = loadConfig()) {
//加载agent.config配置文件
AGENT_SETTINGS.load(configFileStream);
for (String key : AGENT_SETTINGS.stringPropertyNames()) {
String value = (String) AGENT_SETTINGS.get(key);
AGENT_SETTINGS.put(key, PropertyPlaceholderHelper.INSTANCE.replacePlaceholders(value, AGENT_SETTINGS));
}
} catch (Exception e) {
LOGGER.error(e, "Failed to read the config file, skywalking is going to run in default config.");
}
try {
//系统环境变量中取出配置覆盖,没有设定就不进行操作直接返回
overrideConfigBySystemProp();
} catch (Exception e) {
LOGGER.error(e, "Failed to read the system properties.");
}
agentOptions = StringUtil.trim(agentOptions, ',');
if (!StringUtil.isEmpty(agentOptions)) {
try {
agentOptions = agentOptions.trim();
LOGGER.info("Agent options is {}.", agentOptions);
//读取启动命令中的配置项进行第三次配置覆盖更新
overrideConfigByAgentOptions(agentOptions);
} catch (Exception e) {
LOGGER.error(e, "Failed to parse the agent options, val is {}.", agentOptions);
}
}
//对整合好的配置进行分发,初始化探针静态配置
initializeConfig(Config.class);
// reconfigure logger after config initialization
configureLogger();
LOGGER = LogManager.getLogger(SnifferConfigInitializer.class);
//8.14.0新增方法:写入当前java探针版本号
setAgentVersion();
if (StringUtil.isEmpty(Config.Agent.SERVICE_NAME)) {
throw new ExceptionInInitializerError("`agent.service_name` is missing.");
} else {
if (StringUtil.isNotEmpty(Config.Agent.NAMESPACE) || StringUtil.isNotEmpty(Config.Agent.CLUSTER)) {
Config.Agent.SERVICE_NAME = StringUtil.join(
SERVICE_NAME_PART_CONNECTOR,
Config.Agent.SERVICE_NAME,
Config.Agent.NAMESPACE,
Config.Agent.CLUSTER
);
}
}
if (StringUtil.isEmpty(Config.Collector.BACKEND_SERVICE)) {
throw new ExceptionInInitializerError("`collector.backend_service` is missing.");
}
if (Config.Plugin.PEER_MAX_LENGTH <= 3) {
LOGGER.warn(
"PEER_MAX_LENGTH configuration:{} error, the default value of 200 will be used.",
Config.Plugin.PEER_MAX_LENGTH
);
Config.Plugin.PEER_MAX_LENGTH = 200;
}
IS_INIT_COMPLETED = true;
}
这个方法可以看出来:
(1)先通过类的私有loadConfig()方法读取agent.config配置文件,并获取字符流。方法内部会先到环境变量SPECIFIED_CONFIG_PATH中尝试获取配置文件地址,如果没有的话读取默认的地址
DEFAULT_CONFIG_FILE_NAME
将读取的配置key与value放到静态资源对象中
(2)overrideConfigBySystemProp()重载环境变量中预存的配置项,先读取System中的properties资源,如果有重复配置设置则覆盖掉AGENT_SETTINGS中的值;之后再读取启动项中-DskywalkingXXX的配置项,对重复项进行覆盖。
(3)overrideConfigByAgentOptions()读取启动项命令中设置的配置参数进行覆写
(4)initializeConfig()对配置进行初始化,分配到core及各个插件的配置中,里面使用了静态方法ConfigInitializer.initNextLevel()(这是一个递归方法)对获取的配置进行逐层拆解,再将配置聚合到每层下面(每层在配置的key中用 "." 进行分割)。后面的逻辑,包括记录config加载日志,设置版本字段配置,服务名,上报的收集后管配置判断等也属于初始化逻辑,都完成后IS_INIT_COMPLETED设置为true。
2.探针插件加载PluginFinder,作用:遍历读取每个plugin的jar包中skywalking-plugin.def(plugin定义声明文件),获取所有插桩定义Instrumentation类的对象。
这部分的实现主要通过PluginBootstrap和PluginFinder两个类完成的。
PluginBootstrap.loadPlugins()方法源代码如下所示:
public List loadPlugins() throws AgentPackageNotFoundException {
//AgentClassLoader单例初始化,使用懒汉式加载模式
AgentClassLoader.initDefaultLoader();
//读取plugins目录下所有jar包中的定义资源skywalking-plugin.def的绝对路径
PluginResourcesResolver resolver = new PluginResourcesResolver();
List resources = resolver.getResources();
if (resources == null || resources.size() == 0) {
LOGGER.info("no plugin files (skywalking-plugin.def) found, continue to start application.");
return new ArrayList();
}
for (URL pluginUrl : resources) {
try {
PluginCfg.INSTANCE.load(pluginUrl.openStream());
} catch (Throwable t) {
LOGGER.error(t, "plugin file [{}] init failure.", pluginUrl);
}
}
//读取各个插件类定义对象的地址并存到list中
List pluginClassList = PluginCfg.INSTANCE.getPluginClassList();
List plugins = new ArrayList();
//通过类的地址反射创建类定义的实例对象
for (PluginDefine pluginDefine : pluginClassList) {
try {
LOGGER.debug("loading plugin class {}.", pluginDefine.getDefineClass());
AbstractClassEnhancePluginDefine plugin = (AbstractClassEnhancePluginDefine) Class.forName(pluginDefine.getDefineClass(), true, AgentClassLoader
.getDefault()).newInstance();
plugins.add(plugin);
} catch (Throwable t) {
LOGGER.error(t, "load plugin [{}] failure.", pluginDefine.getDefineClass());
}
}
//用来加载动态插件(默认启动下暂时没有用到)
plugins.addAll(DynamicPluginLoader.INSTANCE.load(AgentClassLoader.getDefault()));
//返回类定义的实例对象的列表集合
return plugins;
}
通过debug模式查找可以发现,agent的类加载器AgentClassLoader,在这里初始化的默认加载器中,加载涉及到外部jar包的目录只有:探针程序目录/plugins,/activations两个,配置见下图:
像optional-plugins此类目录下面的插件jar包默认是不加载的,需要调整配置,或者将内部jar包移动到指定加载的目录下。
loadPlugins()执行完后返回类定义(继承自AbstractClassEnhancePluginDefine)的实例对象列表的集合,作为PluginFinder初始化入参
PluginFinder构造方法代码如下:
public PluginFinder(List plugins) {
for (AbstractClassEnhancePluginDefine plugin : plugins) {
//会去读取需要做切面增强的ENHERCE_CLASS的名字,定义在各个插件里
ClassMatch match = plugin.enhanceClass();
if (match == null) {
continue;
}
if (match instanceof NameMatch) {
NameMatch nameMatch = (NameMatch) match;
LinkedList pluginDefines = nameMatchDefine.get(nameMatch.getClassName());
if (pluginDefines == null) {
pluginDefines = new LinkedList();
nameMatchDefine.put(nameMatch.getClassName(), pluginDefines);
}
pluginDefines.add(plugin);
} else {
signatureMatchDefine.add(plugin);
}
if (plugin.isBootstrapInstrumentation()) {
bootstrapClassMatchDefine.add(plugin);
}
}
}
根据对plugin对象的判断,会将define对象分别分配到nameMatchDefine,signatureMatchDefine和bootstrapClassMatchDefine三个列表中
到这里PluginFinder加载基本就完成了。
3.修改类增强,字节码注入(instrumention插桩的inject)
这部分主要是通过byteBuddy提供的api,新建一个AgentBuilder对象,将我们的插件定义pluginFinder实例,agentBuilder实例等都通过字节码修改的方式注入到JVM的引导加载器Bootstrap Class Loader中:
AgentBuilder agentBuilder = new AgentBuilder.Default(byteBuddy).ignore(
nameStartsWith("net.bytebuddy.")
.or(nameStartsWith("org.slf4j."))
.or(nameStartsWith("org.groovy."))
.or(nameContains("javassist"))
.or(nameContains(".asm."))
.or(nameContains(".reflectasm."))
.or(nameStartsWith("sun.reflect"))
.or(allSkyWalkingAgentExcludeToolkit())
.or(ElementMatchers.isSynthetic()));
JDK9ModuleExporter.EdgeClasses edgeClasses = new JDK9ModuleExporter.EdgeClasses();
try {
agentBuilder = BootstrapInstrumentBoost.inject(pluginFinder, instrumentation, agentBuilder, edgeClasses);
} catch (Exception e) {
LOGGER.error(e, "SkyWalking agent inject bootstrap instrumentation failure. Shutting down.");
return;
}
这部分本人资历尚浅,对bytebuddy,asm,javaassit等这些涉及到字节码修改的类库api还不是很深入了解。。。这里bytebuddy处理的大致流程大家可以参考下下面的文章:
Skywalking 8.7.0 源码分析学习笔记-agent - 掘金 (juejin.cn)
4.部分bytebuddy类和skywalking自定义的增强拦截类agent读取边缘打开
try {
agentBuilder = JDK9ModuleExporter.openReadEdge(instrumentation, agentBuilder, edgeClasses);
} catch (Exception e) {
LOGGER.error(e, "SkyWalking agent open read edge in JDK 9+ failure. Shutting down.");
return;
}
这部分当初看了半天没有看懂。。。看了其他大佬的介绍后大概是是解决JDK跨模块问题的一个方法执行。
5.接下来这里的配置Config.Agent.IS_CACHE_ENHANCED_CLASS如果设定为true(配置的加载流程请参考文章上面的内容),将字节码增强修改过的类载入缓存,可供其他探针共用
if (Config.Agent.IS_CACHE_ENHANCED_CLASS) {
try {
agentBuilder = agentBuilder.with(new CacheableTransformerDecorator(Config.Agent.CLASS_CACHE_MODE));
LOGGER.info("SkyWalking agent class cache [{}] activated.", Config.Agent.CLASS_CACHE_MODE);
} catch (Exception e) {
LOGGER.error(e, "SkyWalking agent can't active class cache.");
}
}
注:这个功能,包括之前通过字节码注入,对原生切面类对象的变更和增强,都相当于对原生java程序类定义层面上的更改,name和class都已经被修改,这种情况下,不建议同一个服务植入多个不同三方的探针,因为他们的增强方式不同,可能会产生严重的问题。
6.AgentBuilder插桩对象加载插件的拦截器,一些定义的监听器,agent加载到内存中类的控制策略
等,之后修改探针初始化完成状态为true
agentBuilder.type(pluginFinder.buildMatch())
.transform(new Transformer(pluginFinder))
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.with(new RedefinitionListener())
.with(new Listener())
.installOn(instrumentation);
PluginFinder.pluginInitCompleted();
这里探针的字节码注入就完成了。
7.服务管理中心启动,启动探针内所有服务对象
try {
ServiceManager.INSTANCE.boot();
} catch (Exception e) {
LOGGER.error(e, "Skywalking agent boot failure.");
}
这里的ServiceManager是单例模式,启动方法如下:
public void boot() {
bootedServices = loadAllServices();
prepare();//执行所有加载服务的预执行方法
startup();//执行所有服务的boot方法
onComplete();//执行所有服务的onComplete方法
}
重点是loadAllServices()这个方法,他会去各个jar包(不只是core核心包中,包括某些插件等)的META-INF-service资源目录下搜索org.apache.skywalking.apm.agent.core.boot.BootService资源文件,里面会标注需要加载的所有启动服务类,如下图:
这里是通过注解DefaultImplementor和OverrideImplementor来判断该服务是否需要覆盖其他服务
这个注解会标在服务实现类定义上, 标注OverrideImplementor的服务被加载时,会覆盖掉其定义的默认服务类,每个标注DefaultImplementor的服务只能加载一个实例。
这里有一个坑,如果要自定义一个插件并需要覆盖掉已有的默认启动的服务类,需要明确已加载的插件与自定义的插件不能覆盖同一个默认类,比如在core中定义了默认的数据上报客户端服务TraceSegmentServiceClient,在官方提供的可选插件中有kafka上报的转换插件,提供KafkaTraceSegmentServiceClient用于覆盖默认上报服务,这时候如果想要自定义上报插件,需要在启动目录去掉kafka的插件,防止两者都进行覆盖判断然后出现问题。
8.运行进程挂载shutdown的钩子方法,当探针进程退出时会优先执行ServiceManager事先定义好的停止方法
Runtime.getRuntime()
.addShutdownHook(new Thread(ServiceManager.INSTANCE::shutdown, "skywalking service shutdown thread"));
}
这里当进程被kill的时候,new的独立线程中,会执行服务管理实例对象中的shutdown方法,该方法会依次执行所有已加载服务的shutdown方法后,再执行进程的关闭。
到此agent探针入口premain()方法执行完成,探针所有功能启动,之后会继续加载并启动被监控的服务的jar包到服务启动成功。
下一章将对agent中最重要的部分:ContextManager和TracingContext进行介绍,该部分包括全链路数据采集的主要核心结构,是理解全链路监控体系最重要的部分。