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

1,157 阅读3分钟

我正在参加「掘金·启航计划」

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.png

至于他是怎么生效的,我们后续慢慢说。

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