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的代码实现,或者会有不一样的播种。