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