Java Agent 存在这么一个问题,利用和 Agent 尽管执行时算是一体的,然而实际上 Agent 在 JVM 层面是以 AppClassLoader
类加载器加载的,而利用代码则不肯定。因而当 Agent 中存在利用的加强代码时,容易产生种种问题。OpenTelemetry Agent
为了解决这些问题引入了非凡的机制 muzzle
,本文就将向大家解说muzzle
是如何来解决相似问题的。
Muzzle 的作用
Muzzle is a safety feature of the Java agent that prevents applying instrumentation when a mismatch between the instrumentation code and the instrumented application code is detected.
简略来说,muzzle 是用来在编译时以及运行时进行类和类加载器校验的机制。在 Agent 中独自有一部分代码来实现了这个简单的能力。
至于他是怎么失效的,咱们后续慢慢说。
为什么须要 Muzzle
Muzzle 是一个查看运行时类是否匹配的机制,那么咱们为什么须要这种机制呢?
构想这么一个场景:在利用中援用到了 otel 的 sdk,版本是 1.14.0,而在 Agent 中同样援用了 otel 的 sdk,版本却是 1.15.0,那么理论中产生的抵触怎么办?
再来构想这么一个场景:如果在 Agent 中加强用户代码,然而这部分援用了某个三方 sdk,而这个 sdk 也在利用中应用到了,且版本可能不同,那又要怎么解决?
上述两个场景当然能够应用 shadow
sdk,或者一股脑应用BootstrapClassLoader
来加载类来解决。然而这也会遇到其余的不拘一格的问题。所以 Opentelemetry Java Agent
提供了 muzzle 机制来一劳永逸的解决这个问题。
Muzzle 如何运作
Muzzle 分为两个局部:
- 在编译时,muzzle 会采集应用到的的
helper class
以及第三方的symbols
(蕴含类,办法,变量等等) 援用 - 在运行时他会校验这些援用和实际上
classpath
上援用到的类是否统一
编译时采集
编译时采集借助了 gradle 插件 muzzle-generation
来实现。
Opentelemetry Java Agent 提供了这么一个接口InstrumentationModuleMuzzle
:
public interface InstrumentationModuleMuzzle {Map<String, ClassRef> getMuzzleReferences();
static Map<String, ClassRef> getMuzzleReferences(InstrumentationModule module) {if (module instanceof InstrumentationModuleMuzzle) {return ((InstrumentationModuleMuzzle) module).getMuzzleReferences();} else {return Collections.emptyMap();
}
}
void registerMuzzleVirtualFields(VirtualFieldMappingsBuilder builder);
List<String> getMuzzleHelperClassNames();
static List<String> getHelperClassNames(InstrumentationModule module) {
List<String> muzzleHelperClassNames =
module instanceof InstrumentationModuleMuzzle
? ((InstrumentationModuleMuzzle) module).getMuzzleHelperClassNames()
: Collections.emptyList();
List<String> additionalHelperClassNames = module.getAdditionalHelperClassNames();
if (additionalHelperClassNames.isEmpty()) {return muzzleHelperClassNames;}
if (muzzleHelperClassNames.isEmpty()) {return additionalHelperClassNames;}
List<String> result = new ArrayList<>(muzzleHelperClassNames);
result.addAll(additionalHelperClassNames);
return result;
}
}
这个接口提供了一些办法用于获取 helper class
以及三方类的援用信息等等。对于所有的 InstrumentationModule
,这个接口都会利用一遍。然而这个接口很非凡, 他没有实现类!
InstrumentationModuleMuzzle
没有在代码中间接实现这个接口,而是通过 ByteBuddy
来结构了一个实现。
Agent 通过构建 MuzzleCodeGenerator
实现了 AsmVisitorWrapper
来残缺结构了 InstrumentationModuleMuzzle
的实现办法。因而尽管外表上这个接口没有用,然而通过动静字节码的结构,使得他存在了用途。
运行时查看
运行时查看也是基于 ByteBuddy
实现,Agent 通过实现了 AgentBuilder.RawMatcher
结构了匹配类MuzzleMatcher
。
类实现了 matches
办法,并构建 doesMatch
来应用编译时采集的数据来进行运行时的校验:
private boolean doesMatch(ClassLoader classLoader) {ReferenceMatcher muzzle = getReferenceMatcher();
boolean isMatch = muzzle.matches(classLoader);
if (!isMatch) {MuzzleFailureCounter.inc();
if (muzzleLogger.isLoggable(WARNING)) {
muzzleLogger.log(
WARNING,
"Instrumentation skipped, mismatched references were found: {0} [class {1}] on {2}",
new Object[] {instrumentationModule.instrumentationName(),
instrumentationModule.getClass().getName(),
classLoader
});
List<Mismatch> mismatches = muzzle.getMismatchedReferenceSources(classLoader);
for (Mismatch mismatch : mismatches) {muzzleLogger.log(WARNING, "-- {0}", mismatch);
}
}
} else {if (logger.isLoggable(FINE)) {
logger.log(
FINE,
"Applying instrumentation: {0} [class {1}] on {2}",
new Object[] {instrumentationModule.instrumentationName(),
instrumentationModule.getClass().getName(),
classLoader
});
}
}
return isMatch;
}
值得注意的是,因为 muzzle 查看的开销很大,所以它仅在 InstrumentationModule#classLoaderMatcher()
和 TypeInstrumentation#typeMatcher()
匹配器进行匹配后才执行。muzzle matcher
的后果会在每个类加载器中缓存,因而它只对整个检测模块执行一次。
总结
Otel Agent 破费了微小的精力来构建 muzzle 体系来解决 Agent 和利用之间的类抵触,尽管很简单,然而这部分实现对于用户是暗藏的,所以在应用时用户会感觉很敌对。如果有趣味能够自行钻研一下 muzzle 的代码实现,或者会有不一样的播种。