写点什么

深入探秘 OpenTelemetry Agent 奇特的 muzzle 机制

作者:骑牛上青山
  • 2023-05-13
    上海
  • 本文字数:2504 字

    阅读完需:约 8 分钟

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 的代码实现,或许会有不一样的收获。


发布于: 刚刚阅读数: 2
用户头像

还未添加个人签名 2021-05-18 加入

还未添加个人简介

评论

发布
暂无评论
深入探秘OpenTelemetry Agent奇特的muzzle机制_Java_骑牛上青山_InfoQ写作社区