콘텐츠로 이동

DDtrace 사용자 정의 Instrumentation


작성자: 刘锐

Java Instrumentation 소개

Instrumentation: 계측(일부는 "프로브", 일부는 "埋点"이라고 부르며, 번역 자체는 문제가 없으며 이해하면 됩니다.)

Java Instrumentation은 JavaSE 6의 새로운 기능으로, Java 코드, 즉 java.lang.instrument를 통해 Instrument을 사용하여 Java 코드 방식으로 문제를 해결하는 기능을 구현할 수 있습니다.

Instrumentation을 사용하면 개발자는 애플리케이션과 독립적인 에이전트 프로그램(Agent)을 구축하여 JVM에서 실행 중인 프로그램을 모니터링 및 지원하고, 특정 클래스의 정의를 대체하거나 수정할 수도 있습니다. 이 기능을 통해 개발자는 보다 유연한 런타임 가상 머신 모니터링 및 Java 클래스 조작을 구현할 수 있습니다. 이러한 특성은 사실상 가상 머신 수준에서 지원되는 AOP 구현 방식을 제공하여, 개발자가 JDK를 업그레이드하거나 수정하지 않고도 일부 AOP 기능을 구현할 수 있게 합니다.

JavaSE 6에서 instrumentation 패키지는 더욱 강력한 기능을 제공합니다: 시작 후 instrument, 네이티브 코드 (native code) instrument, 동적 classpath 변경 등이 가능합니다. 이러한 변화는 Java가 더욱 강력한 동적 제어 및 해석 능력을 갖추게 되었음을 의미하며, Java 언어를 더욱 유연하고 다양하게 만듭니다.

DDtrace 사용자 정의 Instrumentation 구조 분석

image.png

  1. Decorator: Instrumentation을 장식하는 데코레이터입니다. BaseDecorator는 기본 데코레이터이므로, 모든 사용자 정의 데코레이터는 BaseDecorator 또는 BaseDecorator의 하위 클래스를 상속해야 합니다. span 조작, 사용자 정의 태그 등의 동작은 BaseDecorator를 통해 구현해야 합니다.
  2. Instrumentation: 계측 프로그램입니다. @AutoService(Instrumenter.class) 어노테이션을 사용하여 현재 클래스를 계측 애플리케이션으로 등록합니다. 에이전트가 시작될 때 @AutoService(Instrumenter.class) 어노테이션이 있는 클래스를 로드합니다.
  3. Advice: Instrumentation이 계측해야 하는 메서드에 대한 증강 처리를 담당합니다. 주로 두 가지 메서드 수준 어노테이션인 @Advice.OnMethodEnter@Advice.OnMethodExit를 제공하며, 각각 메서드 진입 시 호출과 메서드 종료 시 호출을 의미합니다.
  4. Inject/Extract: 주입/추출을 나타내며, 필수 구현은 아닙니다. 주요 기능은 링크 정보의 주입 및 추출 작업입니다. 이를 사용하여 traceid, spanid 및 관련 전파 매개변수와 같은 링크 정보를 전송합니다.

Decorator 클래스 다이어그램

여기서는 일부 클래스 다이어그램을 보여줍니다.

image.png

Instrumentation 클래스 다이어그램

여기서는 일부 클래스 다이어그램을 보여줍니다.

image.png

Instrumenter는 interface로, 풍부한 인터페이스를 제공하며 다양한 정의에 따라 구현됩니다.

image.png

HasAdvice: 메서드에 대한 Around 처리를 수행합니다. 즉, 일반적으로 말하는 埋点 작업을 수행합니다. 주로 하나의 인터페이스 메서드를 제공하며, 계측이 필요한 메서드를 등록하는 데 사용됩니다. 여러 메서드에 대한 埋点 작업을 수행할 수 있습니다.

/**
 * Instrumenters should register each advice transformation by calling {@link
 * AdviceTransformation#applyAdvice(ElementMatcher, String)} one or more times.
 */
void adviceTransformations(AdviceTransformation transformation);

Tracing: trace에 사용됩니다.

/** Parent class for all tracing related instrumentations */
abstract class Tracing extends Default{...}

Profiling: 현재 profiling 계측임을 나타냅니다.

/** Parent class for all profiling related instrumentations */
abstract class Profiling extends Default{...}

CiVisibility: CI 계측 유형입니다.

/** Parent class for all CI related instrumentations */
abstract class CiVisibility extends Default {...}

Default: 기본 구현입니다.

@SuppressFBWarnings("IS2_INCONSISTENT_SYNC")
abstract class Default implements Instrumenter, HasAdvice{...}

Inject 클래스 다이어그램

Setter의 전체 이름은 AgentPropagation.Setter입니다. 여기서는 일부 클래스 다이어그램을 보여줍니다.

image.png

Extract 클래스 다이어그램

ContextVisitor의 전체 이름은 AgentPropagation.ContextVisitor입니다. 여기서는 일부 클래스 다이어그램을 보여줍니다.

image.png

Inject와 Extract 모두 Propagation(전파기)을 포함합니다. 전파기의 사용 및 소개에 대한 참고 문서는 extract + TextMapAdapter를 사용한 사용자 정의 traceId 구현을 참조하십시오.

실전: DDtrace 사용자 정의 Dubbo Instrumentation

통합 설계

image.png

  1. DubboInstrumentation 클래스를 생성하고 계측 관련 정보를 구성합니다.
  2. adviceTransformations를 통해 관련 메서드를 증강합니다. 증강 비즈니스 로직은 RequestAdvice 클래스에서 구현되며, 주로 두 가지 메서드를 구현합니다: @Advice.OnMethodEnter@Advice.OnMethodExit는 각각 메서드 진입 시 호출과 메서드 종료 시 호출을 의미합니다.
  3. DubboDecorator는 데코레이터 역할을 합니다. 예를 들어 span 관련 작업(관련 태그 설정 또는 span 닫기 등)을 수행합니다.
  4. Inject/Extract는 주입/추출을 나타냅니다. 주요 기능은 링크 정보의 주입 및 추출 작업입니다. 이를 사용하여 traceid, spanid 및 관련 전파 매개변수와 같은 링크 정보를 전송합니다. DubboHeadersInjectAdapter는 주로 consumer가 traceId, spanId 등을 전파하는 데 사용되며, provider는 DubboHeadersExtractAdapter를 통해 관련 매개변수를 추출하여 span을 구성합니다.

통합 단계

1 dd-java-agent\instrumentation 디렉토리 아래에 모듈을 생성하고 gradle 방식을 사용하여 생성합니다.

dubbo는 주요 버전에 따라 패키지명, 클래스명, 메서드명이 다르므로, 모듈 생성 시 해당 주요 버전 번호를 포함시키면 유지보수에 유리합니다. 예: dubbo-2.7은 dubbo 2.7 이상 버전을 지원함을 의미하며, 구체적인 버전 지원은 현재 모듈의 build.gradle에서 수정합니다. build.gradle 이름은 유지보수에 불편하므로 여기서는 dubbo-2.7.gradle로 조정합니다.

muzzle {
  pass {
    group = "org.apache.dubbo"
    module = "dubbo"
    versions = "[2.7.0,)"
//    assertInverse = true
  }
}

apply from: "$rootDir/gradle/java.gradle"

apply plugin: 'org.unbroken-dome.test-sets'

dependencies {
  compileOnly(group: 'org.apache.dubbo', name: 'dubbo', version: '2.7.0')
}

testSets {
  latestDepTest {
    dirName = 'test'
  }
}

tasks.withType(Test).configureEach {
  usesService(testcontainersLimit)
}

동시에 settings.gradle 파일에 dubbo-2.7.gradle을 추가합니다.

...
include ':dd-java-agent:instrumentation:dropwizard'
include ':dd-java-agent:instrumentation:dropwizard:dropwizard-views'
include ':dd-java-agent:instrumentation:dubbo-2.7'
include ':dd-java-agent:instrumentation:elasticsearch'
include ':dd-java-agent:instrumentation:elasticsearch:rest-5'
include ':dd-java-agent:instrumentation:elasticsearch:rest-6.4'
include ':dd-java-agent:instrumentation:elasticsearch:rest-7'
...

2 패키지명 datadog.trace.instrumentation.dubbo_2_7x를 생성합니다.

3 계측 클래스 DubboInstrumentation.java를 생성합니다.

package datadog.trace.instrumentation.dubbo_2_7x;

import com.google.auto.service.AutoService;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

import java.util.Map;

import static datadog.trace.agent.tooling.bytebuddy.matcher.ClassLoaderMatchers.hasClassesNamed;
import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.implementsInterface;
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.nameStartsWith;
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.*;

@AutoService(Instrumenter.class)
public class DubboInstrumentation extends Instrumenter.Tracing
    implements Instrumenter.ForTypeHierarchy {

  public DubboInstrumentation() {
    super("apache-dubbo");
  }

//  public static final String CLASS_NAME = "org.apache.dubbo.rpc.Filter";
  public static final String CLASS_NAME = "org.apache.dubbo.monitor.support.MonitorFilter";

  @Override
  public ElementMatcher<ClassLoader> classLoaderMatcher() {
    return  hasClassesNamed(CLASS_NAME);
  }

  @Override
  public ElementMatcher<TypeDescription> hierarchyMatcher() {
    return extendsClass(named(CLASS_NAME));
  }

  @Override
  public void adviceTransformations(AdviceTransformation transformation) {
    transformation.applyAdvice(
        isMethod()
            .and(isPublic())
            .and(nameStartsWith("invoke"))
            .and(takesArguments(2))
            .and(takesArgument(0, named("org.apache.dubbo.rpc.Invoker")))
            .and(takesArgument(1, named("org.apache.dubbo.rpc.Invocation"))),
        packageName + ".RequestAdvice");
  }

  @Override
  public String[] helperClassNames() {
    return new String[]{
        packageName + ".DubboDecorator",
        packageName + ".RequestAdvice",
        packageName + ".DubboHeadersExtractAdapter",
        packageName + ".DubboHeadersInjectAdapter"
    };
  }

  @Override
  public Map<String, String> contextStore() {
    return singletonMap("org.apache.dubbo.rpc.RpcContext", AgentSpan.class.getName());
  }
}

먼저 org.apache.dubbo.rpc.Filter 소스 코드를 살펴보겠습니다.

@SPI
public interface Filter {
    /**
     * Make sure call invoker.invoke() in your implementation.
     */
    Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException;

    interface Listener {

        void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation);

        void onError(Throwable t, Invoker<?> invoker, Invocation invocation);
    }

}

Filter는 interface이므로 implementsInterface 방식을 사용해야 합니다. 이렇게 하면 Filter 인터페이스의 모든 구현을 가로채서 처리할 수 있습니다. org.apache.dubbo.rpc.Filterinvoke 메서드를 제공하며, 두 개의 매개변수 InvokerInvocation을 전달합니다. 이는 나중에 사용됩니다.

void adviceTransformations(AdviceTransformation transformation)를 재정의하여 org.apache.dubbo.rpc.Filter를 가로챕니다.

applyAdvice 매개변수 설명:

  • isMethod(): 메서드를 가로채는 것을 의미합니다.
  • isPublic(): 접근 제어자가 public임을 의미합니다.
  • nameStartsWith("invoke"): 메서드 이름입니다.
  • takesArguments: nameStartsWith("invoke")에 필요한 매개변수 개수입니다.
  • takesArgument: nameStartsWith("invoke")의 관련 매개변수입니다. 필요에 따라 작성하며, 매개변수 유형과 순서가 잘못되면 현재 계측이 무효화됩니다.
  • takesArgument(0, named("org.apache.dubbo.rpc.Invoker")): 첫 번째 매개변수 유형을 나타냅니다.
  • takesArgument(1, named("org.apache.dubbo.rpc.Invocation")): 두 번째 매개변수 유형을 나타냅니다.

helperClassNames(): 보조 클래스입니다. 추가로 사용자 정의한 클래스는 모두 여기서 선언해야 합니다.

Map contextStore(): 컨텍스트 정보 저장에 사용되며, 주로 AgentSpan 또는 AgentScope 관련 정보(예: traceid, spanid 등)를 저장합니다. 여기서 singletonMap("org.apache.dubbo.rpc.Invocation", AgentSpan.class.getName())org.apache.dubbo.rpc.Invocation을 증강하도록 구성합니다.

@AutoService는 google에서 제공하는 SPI 인터페이스 규격으로, 컴파일 타임에 처리됩니다.

계측 클래스는 핵심입니다. 클래스 이름에 @AutoService(Instrumenter.class) 어노테이션을 추가하여 계측 애플리케이션임을 나타냅니다. 애플리케이션을 컴파일하고 패키징할 때 @AutoService(Instrumenter.class) 관련 클래스를 반복하고 관련 클래스 이름을 가져와 META-INF/services/datadog.trace.agent.tooling.Instrumenter라는 파일에 넣습니다. 이 파일은 클래스 로더가 시작될 때 로드됩니다. META-INF/services/datadog.trace.agent.tooling.Instrumenter 파일은 자동으로 생성되며, 일부 코드는 다음과 같습니다.

...
datadog.trace.instrumentation.datastax.cassandra.CassandraClientInstrumentation
datadog.trace.instrumentation.datastax.cassandra4.CassandraClientInstrumentation
datadog.trace.instrumentation.dubbo.DubboInstrumentation
datadog.trace.instrumentation.dubbo_2_7x.DubboInstrumentation
datadog.trace.instrumentation.elasticsearch5.Elasticsearch5RestClientInstrumentation
datadog.trace.instrumentation.elasticsearch6_4.Elasticsearch6RestClientInstrumentation
datadog.trace.instrumentation.elasticsearch7.Elasticsearch7RestClientInstrumentation
datadog.trace.instrumentation.elasticsearch2.Elasticsearch2TransportClientInstrumentation
datadog.trace.instrumentation.elasticsearch5.Elasticsearch5TransportClientInstrumentation
datadog.trace.instrumentation.elasticsearch5_3.Elasticsearch53TransportClientInstrumentation
datadog.trace.instrumentation.elasticsearch6.Elasticsearch6TransportClientInstrumentation
datadog.trace.instrumentation.elasticsearch7_3.Elasticsearch73TransportClientInstrumentation
...

4 DubboDecorator 생성

일부 코드는 다음과 같습니다.

...
public class DubboDecorator extends BaseDecorator {
  private static final Logger log = LoggerFactory.getLogger(DubboDecorator.class);
  public static final CharSequence DUBBO_REQUEST = UTF8BytesString.create("dubbo");

  public static final CharSequence DUBBO_SERVER = UTF8BytesString.create("apache-dubbo");

  public static final DubboDecorator DECORATE = new DubboDecorator();

  public static final String SIDE_KEY = "side";

  public static final String PROVIDER_SIDE = "provider";

  public static final String CONSUMER_SIDE = "consumer";

  public static final String GROUP_KEY = "group";

  public static final String VERSION = "release";
  @Override
  protected String[] instrumentationNames() {
    return new String[]{"apache-dubbo"};
  }

  @Override
  protected CharSequence spanType() {
    return DUBBO_SERVER;
  }

  @Override
  protected CharSequence component() {
    return DUBBO_SERVER;
  }

  public AgentSpan startDubboSpan(Invoker invoker, Invocation invocation) {
    URL url = invoker.getUrl();
    boolean isConsumer = isConsumerSide(url);

    String methodName = invocation.getMethodName();
    String resourceName = generateOperationName(url,invocation);
    String shortUrl = generateRequestURL(url,invocation);
    System.out.println("isConsumer : "+isConsumer);
    if (log.isDebugEnabled()) {
      log.debug("isConsumer:{},method:{},resourceName:{},shortUrl:{},longUrl:{},version:{}",
          isConsumer,
          methodName,
          resourceName,
          shortUrl,
          url.toString(),
          getVersion(url)
          );
    }
    AgentSpan span;
    RpcContext rpcContext = RpcContext.getContext();
    if (isConsumer){
      // this is consumer
      span = startSpan(DUBBO_REQUEST);
    }else{
      // this is provider
      AgentSpan.Context parentContext = propagate().extract(rpcContext, GETTER);
      span = startSpan(DUBBO_REQUEST,parentContext);
    }
    span.setTag("url", url.toString());
    span.setTag("short_url", shortUrl);
    span.setTag("method", methodName);
    span.setTag("dubbo-version",getVersion(url));
    afterStart(span);

    withMethod(span, resourceName);
    if (isConsumer){
      propagate().inject(span, rpcContext, SETTER);
//      InstrumentationContext.get(Invocation.class, AgentSpan.class).put(invocation, span);
    }
    return span;
  }

  public void withMethod(final AgentSpan span, final String methodName) {
    span.setResourceName(methodName);
  }

  @Override
  public AgentSpan afterStart(AgentSpan span) {
    return super.afterStart(span);
  }

    ...
}


...

dubbo는 RPC 프레임워크로서 consumer와 provider가 있습니다. isConsumer를 통해 현재 코드 실행이 consumer인지 provider인지 판단합니다. consumer인 경우 직접 span을 생성합니다. 이 span의 traceid와 parentId는 다른 링크에서 전파되어 전달되며, propagate().inject(span, invocation, SETTER)를 통해 provider로 데이터를 전파합니다. provider인 경우 propagate().extract(invocation, GETTER)를 통해 추출하여 parentContext를 구성하고, parentContext를 통해 현재 span 정보를 구성하여 링크를 연결합니다.

5 RequestAdvice 생성

public class RequestAdvice {

  @Advice.OnMethodEnter(suppress = Throwable.class)
  public static AgentScope beginRequest(@Advice.This Filter filter,@Advice.Argument(0) final Invoker invoker,
                                        @Advice.Argument(1) final Invocation invocation) {

    System.out.println(filter.getClass().getName());
    final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(RpcContext.class);
    if (callDepth > 0) {
      return null;
    }

    AgentScope agentScope = DECORATE.buildSpan(invoker, invocation);
    return agentScope;
  }

  @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
  public static void stopSpan(
      @Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable throwable) {
    if (scope == null) {
      return;
    }
    DECORATE.onError(scope.span(), throwable);
    DECORATE.beforeFinish(scope.span());

    scope.close();
    scope.span().finish();
    CallDepthThreadLocalMap.reset(RpcContext.class);
  }
}

RequestAdvice 클래스는 주로 두 가지 메서드를 구현합니다. 메서드 이름은 사용자 정의할 수 있으며, 두 메서드는 각각 @Advice.OnMethodEnter@Advice.OnMethodExit 어노테이션을 사용하여 메서드 진입 및 종료 시 수행해야 하는 작업을 나타냅니다. CallDepthThreadLocalMap.incrementCallDepth(RpcContext.class)를 통해 메서드 재진입을 방지할 수 있습니다. OnMethodExit 종료 시 CallDepthThreadLocalMap.reset(RpcContext.class)로 규칙을 재설정해야 합니다.

6 컴파일 및 패키징

gradle shadowJar를 사용하여 패키징합니다. 패키징 후 파일은 dd-java-agent\build\libs 디렉토리에 저장됩니다.

소스 코드 주소

<dubbo-instrumentation>

문서 평가

이 페이지가 도움이 되었나요?