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的代码实现,或许会有不一样的收获。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。