Deep Dive into pfinder: Architecture, Features, and Bytecode Instrumentation
This article provides a comprehensive technical overview of pfinder, a Java‑based APM system, covering its core concepts, feature set, comparison with other tracing tools, bytecode instrumentation techniques, plugin architecture, trace‑ID propagation across threads, and a simple hot‑deployment implementation.
In modern software development, performance optimization and fault diagnosis are essential, and Java applications often rely on monitoring tools such as SkyWalking and Zipkin; within JD, the self‑developed pfinder serves as the primary APM solution.
pfinder Overview
pfinder (Problem Finder) is a next‑generation APM system created by the UMP team, offering call‑chain tracing, topology mapping, and multidimensional monitoring without requiring code changes—only two script lines added to the startup file. It supports major JD middleware (jimdb, jmq, jsf) and common open‑source components (Tomcat, HTTP client, MySQL, Elasticsearch).
Key Features
Multidimensional monitoring across data centers, groups, JSF aliases, and callers.
Automatic instrumentation for SpringMVC, JSF, MySQL, JMQ, etc.
Application topology visualization.
Cross‑service call‑chain tracing.
AI‑driven automatic fault analysis.
Traffic recording and replay for testing environments.
Cross‑unit traffic monitoring for JSF.
APM Component Comparison
The table below compares pfinder with Zipkin, Pinpoint, SkyWalking, and CAT, highlighting contributors, implementation methods, integration approaches, transport protocols, OpenTracing support, granularity, global call statistics, trace‑ID queries, alarm capabilities, and JVM monitoring.
Component
Zipkin
Pinpoint
SkyWalking
CAT
pfinder
Contributor
South Korean company
Huawei
Meituan
JD
Implementation
Request interception, HTTP/MQ to Zipkin service
Bytecode injection
Bytecode injection
Proxy instrumentation (interceptor, annotation, filter)
Bytecode injection
Integration
Linkerd/Sleuth config
javaagent bytecode
javaagent bytecode
Code intrusion
javaagent bytecode
Transport
HTTP, MQ
Thrift
gRPC
HTTP/TCP
JMTP
OpenTracing
Supported
Supported
Supported
Granularity
Interface level
Method level
Method level
Code level
Method level
Global Call Stats
Supported
Supported
Supported
Supported
TraceID Query
Supported
Supported
Supported
Alarm
Supported
Supported
Supported
Supported
JVM Monitoring
Supported
Supported
Supported
Supported
pfinder also provides built‑in support for JD’s internal components such as jsf, jmq, and jimdb.
Bytecode Modification Techniques
Four popular Java bytecode manipulation frameworks—ASM, Javassist, ByteBuddy, and ByteKit—are demonstrated by implementing a simple start/end logging functionality.
ASM Implementation
@Override
public void visitCode() {
super.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("start");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
@Override
public void visitInsn(int opcode) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
// method return, print "end"
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("end");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
mv.visitInsn(opcode);
}Javassist Implementation
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("com.ggc.javassist.HelloWord");
CtMethod m = cc.getDeclaredMethod("printHelloWord");
m.insertBefore("{ System.out.println(\"start\"); }");
m.insertAfter("{ System.out.println(\"end\"); }");
Class c = cc.toClass();
cc.writeFile("/path/to/classes/com/ggc/javassist");
HelloWord h = (HelloWord) c.newInstance();
h.printHelloWord();ByteBuddy Implementation
// Dynamically generate a new HelloWord class
Class
dynamicType = new ByteBuddy()
.subclass(HelloWord.class)
.method(ElementMatchers.named("printHelloWord"))
.intercept(MethodDelegation.to(LoggingInterceptor.class))
.make()
.load(HelloWord.class.getClassLoader())
.getLoaded();
HelloWord dynamicService = (HelloWord) dynamicType.newInstance();
dynamicService.printHelloWord();ByteKit Implementation
// Parse interceptor class and apply enhancements
DefaultInterceptorClassParser interceptorClassParser = new DefaultInterceptorClassParser();
List
processors = interceptorClassParser.parse(HelloWorldInterceptor.class);
ClassNode classNode = AsmUtils.loadClass(HelloWord.class);
for (MethodNode methodNode : classNode.methods) {
MethodProcessor methodProcessor = new MethodProcessor(classNode, methodNode);
for (InterceptorProcessor interceptor : processors) {
interceptor.process(methodProcessor);
}
}A comparison table then evaluates performance, ease of use, and functionality of the four frameworks.
Bytecode Injection via Java Agents
Java agents can be built using JVMTI (native) or the java.lang.instrument API (pure Java). The article explains JVMTI’s Agent_OnLoad, Agent_OnAttach, and Agent_OnUnload functions and shows how IDEs like IntelliJ IDEA use the JDWP agent for debugging.
Instrument API Example (MANIFEST.MF)
// Specify premain class
com.ggc.agent.GhlAgent
com.ggc.agent.GhlAgent
true
trueAgent Main Class
public class GhlAgent {
public static Logger log = LoggerFactory.getLogger(GhlAgent.class);
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
log.info("agentmain method");
boot(instrumentation);
}
public static void premain(String agentArgs, Instrumentation instrumentation) {
log.info("premain method");
boot(instrumentation);
}
private static void boot(Instrumentation instrumentation) {
new AgentBuilder.Default()
.type(ElementMatchers.nameStartsWith("com.jd.aviation.performance.service.impl"))
.transform((builder, typeDescription, classLoader, javaModule) ->
builder.method(ElementMatchers.isMethod().and(ElementMatchers.isPublic()))
.intercept(MethodDelegation.to(TimingInterceptor.class)))
.installOn(instrumentation);
}
}Timing Interceptor
public class TimingInterceptor {
public static Logger log = LoggerFactory.getLogger(TimingInterceptor.class);
@RuntimeType
public static Object intercept(@SuperCall Callable
callable) throws Exception {
long start = System.currentTimeMillis();
try {
return callable.call();
} finally {
long end = System.currentTimeMillis();
log.info("Method call took {} ms", (end - start));
}
}
}pfinder Implementation Details
When the pfinder agent starts, it loads service and plugin configuration files, performs bytecode enhancement via plugins, and reports data using JMTP. Service loading creates a SimplePFinderServiceLoader instance, while plugin loading parses service definitions, matchers, and interceptors.
During plugin initialization, the PluginRegistrar builds a combined type matcher chain and uses ByteBuddy’s AgentBuilder to apply transformations, ignoring synthetic classes and known libraries.
Trace‑ID Propagation Across Threads
pfinder stores the trace‑ID in MDC (ThreadLocal). To avoid loss in asynchronous execution, it wraps Runnable instances with TracingRunnable , transferring the snapshot to the child thread and restoring the context before execution.
public class TracingRunnable implements PfinderWrappedRunnable {
private final Runnable origin;
private final TracingSnapshot
snapshot;
private final Component component;
private final String operationName;
private final String interceptorName;
private final InterceptorClassLoader interceptorClassLoader;
public TracingRunnable(Runnable origin, TracingSnapshot
snapshot, Component component,
String operationName, String interceptorName,
InterceptorClassLoader interceptorClassLoader) {
this.origin = origin;
this.snapshot = snapshot;
this.component = component;
this.operationName = operationName;
this.interceptorName = interceptorName;
this.interceptorClassLoader = interceptorClassLoader;
}
public void run() {
TracingContext tracingContext = ContextManager.tracingContext();
if (tracingContext.isTracing() && tracingContext.traceId().equals(this.snapshot.getTraceId())) {
this.origin.run();
return;
}
LowLevelAroundTracingContext context = SpringAsyncTracingContext.create(
this.operationName, this.interceptorName, this.snapshot, this.interceptorClassLoader, this.component);
context.onMethodEnter();
try {
this.origin.run();
} catch (RuntimeException ex) {
context.onException(ex);
throw ex;
} finally {
context.onMethodExit();
}
}
}Hot Deployment
Using the javaagent, pfinder can perform class search, decompilation, and hot‑update at runtime. The article shows screenshots of these capabilities and notes limitations such as lack of support for Spring XML/MyBatis XML and the inherent constraints of the Instrumentation API, which cannot add fields or change class hierarchies without DCEVM.
Overall, the article offers a thorough technical guide to pfinder’s design, instrumentation mechanisms, and practical extensions for tracing, monitoring, and dynamic code updates.
JD Tech
Official JD technology sharing platform. All the cutting‑edge JD tech, innovative insights, and open‑source solutions you’re looking for, all in one place.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.