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

騎牛上青山發表於2023-05-13

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的程式碼實現,或許會有不一樣的收穫。

相關文章