콘텐츠로 이동

APM을 사용하여 완전한 클래스 함수 호출을 추적하는 방법

일반적으로 애플리케이션에 APM을 연결하면 Tomcat, Redis, MySQL과 같은 애플리케이션 관련 컴포넌트 및 서비스 간의 호출 추적(분산 추적)을 확인할 수 있습니다. 이는 APM이 표준 컴포넌트에 대해 인스트루먼테이션을 수행하여 실제 사용 과정에서 컴포넌트 호출이 애플리케이션에 미치는 영향을 더 잘 관찰할 수 있도록 하기 때문입니다.

하지만 실제 운영 환경에서는 비표준 코드, 즉 비즈니스 코드가 종종 애플리케이션에 더 큰 영향을 미칩니다. 개발자마다 코드 이해도와 작성 능력에 차이가 있기 때문에 비즈니스 코드의 경우, 완전한 클래스 함수 호출을 추적하여 핵심 문제를 찾는 것이 여전히 매우 중요합니다.

다행히도 APM 개발자들도 이 분야의 전문가로서 이러한 어려움을 잘 알고 있으며, APM에 더 높은 기대를 걸고 있습니다. DataDog과 OpenTelemetry도 관련 기능을 제공하고 있으니 함께 살펴보겠습니다.

JAVA를 예시로 들어 DDTrace(DataDog)와 OpenTelemetry를 각각 살펴보겠습니다.

DDTrace를 사용하여 함수 호출을 추적하는 방법

다음과 같은 코드를 준비합니다.

    @Autowired
    private TestService testService;

    @GetMapping("/user")
    @ResponseBody
    public String getUser(){
        logger.info("do getUser");
        return testService.users();
    }

Service 인터페이스

public interface TestService {

    String users();
}

Service 구현 클래스

package com.zy.observable.server.service;

import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
public class TestServiceImpl implements TestService {
    private static final Logger logger = LoggerFactory.getLogger(TestServiceImpl.class);
    public String getUsername(){
        return "lr";
    }

    public String users(){
        Map<Integer,Student> users =new HashMap<>();
        users.put(1,new Student("tom",18));
        users.put(2,new Student("joy",20));
        users.put(3,new Student("lucy",30));
        users.forEach((k,v)->print(k,v));
        return getUsername();
    }

    public void print(Integer level,Student student){
        logger.info("level:{},username:{}",level,student.getUsername());
    }
}

Controller 계층에서 testService.users()를 호출할 때, 기본적으로 users는 추적(Trace)의 일부로 포함되지 않습니다. 다음 명령어로 실행합니다.

java -javaagent:D:/ddtrace/dd-java-agent-1.25.2-guance.jar \
-Ddd.service=springboot-server \
-Ddd.env=1.0 \
-Ddd.agent.port=9529 \
-jar springboot-server.jar
또는 IDEA 도구에서 디버깅할 수도 있습니다.

http://localhost:8090/user를 요청하면 Guance 플랫폼에서 다음과 같은 결과를 확인할 수 있습니다.

Img

추적 상황을 관찰할 수 있으며, 두 개의 스팬(Span)이 있습니다.

users 메서드를 추적에 포함시키려면 DDTrace에서 다음 매개변수를 제공하여 비즈니스 코드 정보를 찾을 수 있습니다.

  • 매개변수 방식: -Ddd.trace.methods
  • 환경 변수 방식: DD_TRACE_METHODS

다음 명령어를 사용합니다.

java -javaagent:D:/ddtrace/dd-java-agent-1.25.2-guance.jar \
-Ddd.service=springboot-server \
-Ddd.env=1.0 \
-Ddd.agent.port=9529 \
-Ddd.trace.methods="com.zy.observable.server.service.TestService[users]" \
-jar springboot-server.jar

Guance 플랫폼에서 다음과 같은 결과를 확인할 수 있습니다.

Img

클래스의 모든 함수 호출을 추적하려면 *를 사용하여 나타냅니다.

java -javaagent:D:/ddtrace/dd-java-agent-1.25.2-guance.jar \
-Ddd.service=springboot-server \
-Ddd.env=1.0 \
-Ddd.agent.port=9529 \
-Ddd.trace.methods="com.zy.observable.server.service.TestService[*]" \
-jar springboot-server.jar

Guance 플랫폼에서 다음과 같은 결과를 확인할 수 있습니다.

Img

TestService에는 인터페이스 메서드가 하나만 있지만, 와일드카드 *를 사용하면 여러 개의 스팬 정보가 나타납니다.

세심한 분들은 문제를 발견하셨을 수도 있습니다. TestServiceImplgetUsername, users, print 세 개의 함수를 제공하며, users는 각각 getUsernameprint를 호출합니다. 그런데 추적(Trace)에는 print가 있지만 getUsername이 없는 이유는 무엇일까요?

  • 추적(Trace)에 print가 있는 이유

TestServiceImplTestService 인터페이스를 구현하며, *는 모두를 의미합니다. DDTrace는 실제로 TestService의 구현 클래스인 TestServiceImpl을 인스트루먼트합니다. 이는 -Ddd.trace.methods="com.zy.observable.server.service.TestServiceImpl[*]"와 동일합니다.

  • 추적(Trace)에 getUsername이 없는 이유

주로 DDTrace가 일부 핵심 메서드를 차단하기 때문입니다. 다음 유형의 메서드 함수는 인스트루먼트되지 않습니다.

  • 생성자(constructors)
  • getters
  • setters
  • synthetic
  • toString
  • equals
  • hashcode
  • finalizer 메서드 호출

주의할 점은 위의 추적(Trace)에서 print 함수가 세 개의 스팬을 나타내는 이유는 stus가 Map 컬렉션이고, 루프가 세 번 실행되면서 print가 세 번 호출되었기 때문입니다. 이는 특정 시나리오에서 치명적일 수 있습니다. 만약 stus 컬렉션이 1000개라면 print는 해당하는 수만큼의 스팬을 생성합니다. 이는 추적(Trace)을 확인하거나 비용 측면에서 매우 높은 대가를 치르게 됩니다. 전자의 경우 스팬이 너무 많으면 브라우저 메모리 사용량이 높아져 UI가 느려지고, 후자의 경우 저장 및 쿼리 비용이 증가합니다.

OpenTelemetry를 사용하여 함수 호출을 추적하는 방법

위의 코드를 그대로 사용하고, 매개변수를 추가하지 않고 다음 명령어로 애플리케이션을 실행합니다.

-javaagent:/home/liurui/agent/opentelemetry-javaagent-1.26.1-guance.jar
-Dotel.traces.exporter=otlp
-Dotel.exporter.otlp.endpoint=http://localhost:4317
-Dotel.resource.attributes=service.name=springboot-server

Guance 플랫폼에서 다음과 같은 결과를 확인할 수 있습니다.

Img

OpenTelemetry의 추적(Trace) 스팬이 DDTrace와 기본적으로 동일하다는 것을 확인할 수 있습니다.

코드를 수정할 수 없는 경우, OpenTelemetry는 특정 메서드의 스팬을 캡처하기 위해 다음과 같은 Java 에이전트 구성을 제공합니다.

  • 매개변수 방식: -Dotel.instrumentation.methods.include
  • 환경 변수 방식: OTEL_INSTRUMENTATION_METHODS_INCLUDE

참고로, OpenTelemetry는 와일드카드 방식의 구성을 지원하지 않습니다.

다음 명령어를 실행합니다.

-javaagent:/home/liurui/agent/opentelemetry-javaagent-1.26.1-guance.jar
-Dotel.traces.exporter=otlp
-Dotel.exporter.otlp.endpoint=http://localhost:4317
-Dotel.resource.attributes=service.name=springboot-server
-Dotel.instrumentation.methods.include="com.zy.observable.server.service.TestService[users]"

Guance 플랫폼에서 다음과 같은 결과를 확인할 수 있습니다.

Img

마찬가지로 users 함수에도 스팬 정보가 추가되었습니다.

SDK 방식

위에서는 DDTrace와 OpenTelemetry가 비침투적 방식으로 비즈니스 특정 함수 메서드에 대한 분산 추적을 수행하는 방법을 각각 소개했습니다. 또한 두 도구 모두 SDK 방식을 제공하며, 이는 어느 정도 코드 침투성이 있지만 더 유연하게 동작합니다.

  • DDTrace는 @Trace 애노테이션을 사용하여 비즈니스 Span을 구성합니다. 실제 사용법 및 관련 종속성은 링크를 참조하세요.
  • OpenTelemetry는 @WithSpan 애노테이션을 사용하여 비즈니스 Span을 구성합니다. 링크를 참조하세요.

SDK 방식에 대한 자세한 분석은 여기서 생략하겠습니다.

참고 문서

DDTrace Agent 다운로드 주소

OpenTelemetry Agent 다운로드 주소

OpenTelemetry methods

SpringBoot-Server demo

문서 평가

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