티스토리 뷰

최근에 회사에서 AWS CloudWatch Log를 이용하여 애플리케이션에서 발생되는 로그를 볼 수 있게끔 구성하였다.

 

이번에는 애플리케이션이 처리하는 요청/응답에 대한 데이터를 수집하고 또 수집되는 과정에서 오류가 발생 시 원인을 쉽고 간편하게 파악할 수 있는 AWS X-RAY에 대해 알아보도록 하자.

 

그리고 CloudWatch에서는 CloudWatch ServiceLens라는 걸 제공해주는데 CloudWatch ServiceLens는 지표, 로그, 경보, 기타 리소스 상태정보를 한 곳에서 관리할 수 있으며 AWS X-RAY와 통합하여 애플리케이션에 대한 전체적인 서비스맵을 제공하여 한눈에 볼 수 있도록 기능을 제공한다.

 

AWS X-RAY를 JAVA 웹 애플리케이션에 적용하기 위해서는 X-RAY SDK가 필요한데 여기를 참고하자

실습환경

  • JAVA 11
  • Spring Boot 2.6.2
  • Spring AOP
  • Spring Data JPA
  • Amazon EC2 (Region : ap-northeast-2)
  • Amazon Cloud Watch Logs
  • Docker
  • MySQL 8.0

실습은 위 환경으로 진행하며 내용의 양이많기 때문에 순서대로 진행할 것을 권장한다.

 

EC2 인스턴스 생성

#!/bin/bash
amazon-linux-extras install -y java-openjdk11
yum install -y docker
systemctl start docker
docker pull mysql
docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=1234 $(docker images | grep -v awk | awk '/mysql/ {print $3}')
curl https://s3.ap-northeast-2.amazonaws.com/aws-xray-assets.ap-northeast-2/xray-daemon/aws-xray-daemon-3.x.rpm -o /home/ec2-user/xray.rpm
yum install -y /home/ec2-user/xray.rpm

EC2 인스턴스를 생성할 때 '3. 인스턴스 구성' 메뉴에서 사용자 데이터에 해당 쉘 명령어를 추가해주면 인스턴스가 생성될 때 해당 쉘 스크립트에 작성된 명령어들이 실행된다. 그리고 보안그룹에 8080 포트를 넣어주고 인스턴스를 생성하도록 하자. EC2 인스턴스 생성하는 방법은 여기를 보자

 

밑에서 첫번째 두번째 명령어는 X-RAY 데몬을 다운받고 설치하는 명령어인데 X-RAY 데몬이란 애플리케이션에서 생성되는 세그먼트의 데이터를 수집하여 X-RAY로 전송해주는 역할을 한다. X-RAY 데몬은 AWS X-RAY SDK가 실행중이어야 SDK에서 전송하는 데이터가 X-RAY로 전송된다.



java -version
docker --version
sudo docker images
sudo docker ps
ps -ef | grep xray

JAVA, X-RAY 데몬, Docker, MySQL Container가 정상적으로 설치 및 실행된 지 확인한 결과 정상적으로 되었다.

EC2 인스턴스를 재부팅하면 X-RAY 데몬은 종료되는데 'xray -o -n Region명' 명령어로 실행해주면 된다.

IAM 생성

AWSXRayDaemonWriteAccess Role을 가진 역할을 생성하여 EC2 인스턴스에 해당 IAM 역할을 부여해준다.

X-RAY Dependencies 추가

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-xray-recorder-sdk-bom</artifactId>
            <version>2.10.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-xray-recorder-sdk-core</artifactId>
</dependency>
<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-xray-recorder-sdk-apache-http</artifactId>
</dependency>
<dependency>
      <groupId>com.amazonaws</groupId>
      <artifactId>aws-xray-recorder-sdk-aws-sdk</artifactId>
</dependency>
<dependency>
      <groupId>com.amazonaws</groupId>
    <artifactId>aws-xray-recorder-sdk-sql</artifactId>
</dependency>
<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-xray-recorder-sdk-slf4j</artifactId>
</dependency>
dependencyManagement {
    imports {
        mavenBom('com.amazonaws:aws-xray-recorder-sdk-bom:2.10.0')
    }
}

dependencies {
    compile("com.amazonaws:aws-xray-recorder-sdk-core")
    compile("com.amazonaws:aws-xray-recorder-sdk-apache-http")
    compile("com.amazonaws:aws-xray-recorder-sdk-aws-sdk")
    compile("com.amazonaws:aws-xray-recorder-sdk-sql")
    compile("com.amazonaws:aws-xray-recorder-sdk-slf4j")
}

필자는 JAVA 11, Spring Boot 2.6.2 버전으로 생성하였고 생성 후 X-RAY SDK를 추가해준다.

각 모듈에 대한 설명

aws-xray-recorder-sdk-core

  • 세그먼트 생성 및 전송을 위해 필요하며 사용자의 요청을 받을 때 세그먼트를 구성할 AWSXrayServletFilter가 있습니다.

aws-xray-recorder-sdk-apache-http

  • 애플리케이션에서 또다른 HTTP API를 요청할 때 해당 요청을 계측합니다.

aws-xray-recorder-sdk-aws-sdk

  • 애플리케이션의 요청 및 응답 데이터를 AWS X-RAY로 보내기 위한 모듈입니다.

aws-xray-recorder-sdk-sql

  • 데이터베이스에 요청한 SQL수행시간 및 DBMS에 대한 정보를 볼 수 있습니다.

aws-xray-recorder-sdk-slf4j

  • 로그메세지에 트레이스 ID를 주입시킵니다. 트레이스 ID는 뒤에서 설명드리겠습니다.

 

샘플링 규칙

src/main/resources디렉토리에 sampling-rules.json 파일을 작성하여 다음과 같은 샘플링 규칙을 작성하자.

 

rules

  • 각 요청에 대해 여러 샘플링 규칙 룰을 정의할 수 있다.

host

  • 규칙을 적용할 호스트이름을 적는다. 모든 호스트에 대해 적용하려면 * 처리하면된다.

http_method

  • 특정 HTTP Method 요청에 대해서만 수집할 수 있으며 모든 요청에 대해 적용하려면 * 처리하면된다.

url_path

  • 지정된 요청에 대해서 데이터를 수집한다. /api/* 라고 적게되면 HTTP 요청url중 /api/으로 시작된 요청에 대해서만 수집한다.

fixed_target

  • 샘플링 규칙이 샘플링 할 임의의 초당 트레이스 수를 의미한다. 해당 값을 0으로 세팅하면 지정된 요청에 대해서는 데이터를 수집하지 않는다.

rate

  • fixedTarget에 도달한 후 규칙이 샘플링해야 하는 비율을 의미한다. 값을 1로하면 100%로 수집하게되며 0.1로 하게 될 경우 10%만 수집한다.

default

  • rules에 적용되지 않은 모든 요청에 처리한다

 

CloudWatch Log에 트레이스 ID 주입하기

로그패턴에 %X{AWS-XRAY-TRACE-ID} 를 넣어주자 그러면 CloudWatch Log에서 해당 트레이스 ID를 주입시켜주며 트레이스 ID로 특정 HTTP 요청 및 응답에 대한 정보를 볼 수 있다.

 

CloudWatch로 Log를 전송하는 방법은 여기를 참고하자

 

X-RAY 설정

aws:
  xray:
    fixed-segment-name: xray-application
    prefix-log-name: AWS-XRAY
    sampling-rules-json: /sampling-rules.json
    datasource:
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: 1234
      jdbc-url: jdbc:mysql://localhost:3306/mysql?useSSL=false&serverTimezone=UTC&characterEncoding=utf8&allowPublicKeyRetrieval=true
import com.amazonaws.xray.AWSXRay;
import com.amazonaws.xray.AWSXRayRecorderBuilder;
import com.amazonaws.xray.javax.servlet.AWSXRayServletFilter;
import com.amazonaws.xray.plugins.EC2Plugin;
import com.amazonaws.xray.slf4j.SLF4JSegmentListener;
import com.amazonaws.xray.strategy.ContextMissingStrategy;
import com.amazonaws.xray.strategy.sampling.LocalizedSamplingStrategy;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import javax.servlet.Filter;
import java.net.URL;

@Configuration
public class XRaySegmentConfig {
    @Value("${aws.xray.fixed-segment-name}")
    private String fixedSegmentName;

    @Value("${aws.xray.prefix-log-name}")
    private String prefixLogName;

    @Value("${aws.xray.sampling-rules-json}")
    private String samplingRulesJson;

    @PostConstruct
    public void init() {
        AWSXRay.beginSegment(fixedSegmentName);

        URL ruleFile = getClass().getResource(samplingRulesJson);

        AWSXRayRecorderBuilder builder = AWSXRayRecorderBuilder.standard().withPlugin(new EC2Plugin());
        builder.withSamplingStrategy(new LocalizedSamplingStrategy(ruleFile));
        builder.withSegmentListener(new SLF4JSegmentListener(prefixLogName));
        builder.withContextMissingStrategy(new IgnoreContextMissingStrategy());
        AWSXRay.setGlobalRecorder(builder.build());

        AWSXRay.endSegment();
    }

    @Bean
    public Filter tracingFilter() {
        return new AWSXRayServletFilter(fixedSegmentName);
    }

    private class IgnoreContextMissingStrategy implements ContextMissingStrategy {
        @Override
        public void contextMissing(String message, Class<? extends RuntimeException> exceptionClass) {}
    }
}

위 코드의 IgnoreContextMissingStrategy에 대해 간략히 설명하자면 다음과 같다

  • ContextMissing이 발생했을 때 SegmentNotFoundException이 발생되는데 이를 무시해준다.
  • Java System 속성 'com.amazonaws.xray.strategy.contextMissingStrategy'에 'IGNORE_ERROR' 라는 값
    을 정의해주면 IgnoreErrorContextMissingStrategy.class 인스턴스가 생성되어 런타임 예외나 에러로그가 발생하진 않지만 그냥 이그노어 해주는 별도의 클래스를 만들었다. 그렇다면 어떤 런타임 예외가 나는지 아래 DataSource Java Config 코드를 보자.

 

X-RAY DataSource 설정

import com.amazonaws.xray.sql.TracingDataSource;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;

@Configuration
public class XRayDataSourceConfig {
    @Bean
    @ConfigurationProperties(prefix = "aws.xray.datasource")
    public HikariConfig hikariConfig() {
        return new HikariConfig();
    }
    @Bean
    public DataSource tracingDataSource(HikariConfig hikariConfig) {
        return TracingDataSource.decorate(new HikariDataSource(hikariConfig));
    }
}

해당 예외는 애플리케이션 구동 시 Segment가 생성되기도전에 Subsegment를 생성하려고 할 때 발생되는데 DB와 커넥션을 맺을 때 발생된다. DB에 요청한 SQL수행시간 및 DBMS에 대한 정보를 볼때 DataSource를 한번 래핑한 TracingDataSource로 Bean을 구성해야하는데 이렇게 구성하게되면 DB에 요청이 전송될 때 Subsegment가 생성되기 때문이다.

 

X-RAY Apache Http Client 설정

import com.amazonaws.xray.proxies.apache.http.HttpClientBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

@Configuration
public class XRayHttpClientConfig {
    private static final int DEFAULT_MAX_PER_ROUTE = 200;
    private static final int MAX_TOTAL = 50;

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate(clientHttpRequestFactory());
    }

    private HttpComponentsClientHttpRequestFactory clientHttpRequestFactory() {
        PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager();
        manager.setDefaultMaxPerRoute(DEFAULT_MAX_PER_ROUTE);
        manager.setMaxTotal(MAX_TOTAL);

        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        CloseableHttpClient httpClient = HttpClientBuilder.create().setConnectionManager(manager).build();
        factory.setHttpClient(httpClient);
        return factory;
    }
}

생성된 세그먼트 또는 서브세그먼트에서 HttpClient를 이용하여 다른 API를 요청할 때 해당 요청을 계측할 수 있도록 HttpClient를 구성한다.

 

X-RAY SQL 추적하기

공식문서를 보면 기본적으로 보안상 이유로 실행된 SQL을 추적하진 않는다.

만약 SQL을 추적하려면 aws-xray-recorder-sdk-sql-(mysql | postgres) 둘중 하나를 추가 해야한다.

 

왜냐면 SQL을 추적하려면 jdbc interceptor를 제공해야 하는데 Spring Boot의 HikariPool에서는 지원을 하지 않는다.

그래서 Connection Pool을 Tomcat Jdbc Pool을 써야한다.

 

aws-xray-recorder-sdk-sql-(mysql | postgres) 모듈에서 TracingInterceptor라는 클래스를 제공하는데

이 클래스가 바로org.apache.tomcat.jdbc.pool.JdbcInterceptor를 상속한 클래스다.

 

하지만 !

최신버전에서는 위 작업을 하지않아도 쿼리에 대한 정보를 가져올 수 있다. Java System 속성 'com.amazonaws.xray.collectSqlQueries' 또는 OS 환경변수 'AWS_XRAY_COLLECT_SQL_QUERIES'에 'true' 값을 지정해주면 된다. 

 

Spring AOP를 이용하여 X-RAY 세그먼트 생성하기

Client로부터 요청이 오면 세그먼트가 생성되며 생성된 세그먼트로부터 서브 세그먼트를 생성할 수 있다. 이 서브 세그먼트에는 요청 및 응답에 대한 정보를 기록할 수 있도록 구성할 것 이다.

 

기록하려는 정보는 다음과 같다

  • 호출된 클래스 및 메서드 정보
  • 메서드 파라미터에 대한 정보
  • 에러가 발생했다면 에러에 대한 상세내용

공식문서에서도 Spring AOP를 이용한 예제샘플이 있는데 이 예제코드를 돌릴려면 무조건 Spring Data JPA 모듈이 있어야지만 돌아간다.. AbstractXRayInterceptor 라는 클래스가 제공되는데 @Pointcut에 Spring Data JPA에서 제공하는 Repository 인터페이스가 걸려있기 때문이다. 그래서 이를 무시하고 새로 다시 코드를 작성했다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
import com.amazonaws.xray.AWSXRay;
import com.amazonaws.xray.entities.Subsegment;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import java.lang.reflect.Parameter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Aspect
@Component
public class XrayAspect {
    private static final String DELIMITER = " || ";
    private static final String TARGET_CONTROLLER = "execution(* com.example.awsxray.controller..*Controller.*(..))";
    private static final String TARGET_SERVICE = "execution(* com.example.awsxray.service..*Service.*(..))";
    private static final String TARGET_REPOSITORY = "execution(* org.springframework.data.jpa.repository.JpaRepository+.*(..))";
    private static final String TARGET_ADVICE = TARGET_CONTROLLER + DELIMITER + TARGET_SERVICE + DELIMITER + TARGET_REPOSITORY;

    @Pointcut(value = TARGET_ADVICE)
    public void xrayTargetClass() {

    }

    @Around(value = "xrayTargetClass()")
    public Object processTargetXrayTrace(ProceedingJoinPoint joinPoint) throws Throwable {
        Object proceed;
        try {
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            String subsegmentName = getSubsegmentName(methodSignature);

            Subsegment subsegment = AWSXRay.beginSubsegment(subsegmentName);
            if (subsegment == null) {
                return null;
            }
            Parameter[] parameters = methodSignature.getMethod().getParameters();
            Object[] args = joinPoint.getArgs();

            Map<String, Map<String, Object>> metadata = new HashMap<>();
            requestTrace(metadata, parameters, args);

            proceed = args.length == 0 ? joinPoint.proceed() : joinPoint.proceed(args);

            subsegment.setMetadata(metadata);
            return proceed;
        } catch (Throwable throwable) {
            AWSXRay.getCurrentSubsegmentOptional().ifPresent(currentSubsegment -> {
                currentSubsegment.addException(throwable);
            });
            throw throwable;
        } finally {
            AWSXRay.endSubsegment();
        }
    }

    private String getSubsegmentName(MethodSignature methodSignature) {
        Class className = methodSignature.getDeclaringType();
        String methodName = methodSignature.getName();
        return className + "." + methodName;
    }

    private void requestTrace(Map<String, Map<String, Object>> metadata, Parameter[] parameters, Object[] args) {
        Map<String, Object> request = new HashMap<>();
        for (int i = 0; i < parameters.length; i++) {
            request.put(parameters[i].getName(), ObjectUtils.nullSafeToString(args[i]));
        }
        metadata.put("request", request);
    }
}

위 코드는 다음과 같은 기능을 합니다.

  • "클래스명.메서드명" 이름으로 서브 세그먼트를 생성합니다.
  • "request" 라는 곳에 파라미터명과 파라미터값을 기록합니다.
  • 예외가 발행되면 현재 서브 세그먼트에 예외정보를 담습니다.
  • 서브 세그먼트를 종료합니다. 서브 세그먼트를 종료하지 않으면 예외가 발생되니 예외가 발생되도 finally에서 종료해줍니다.

 

JAVA 웹 애플리케이션 작성

간단한 Rest API를 구축하였습니다. 코드는 하단에 github 링크 첨부해드릴테니 참조 바랍니다.

 

애플리케이션 요청

Xray의 리소스를 생성하는 POST API를 요청했습니다.

 

 

트레이스 ID가 로그에 심어진 걸 확인하였고 저 ID를 복사하여 X-RAY쪽에 검색하러 갑니다.

 

 

사진처럼 각 레이어별로 서브 세그먼트가 생성된 걸 볼 수 있습니다. 

 

 

요청 및 응답에 대한 정보와 API 수행시간에 대한 정보를 볼 수 있습니다.

 

그리고 어떤 클래스의 어떤 메서드가 호출된지 볼 수 있고 request 정보를 확인하실 수 있습니다.

 

파라미터가 바인딩 된 쿼리는 확인이 안되지만 바인딩 전 쿼리는 확인이 가능합니다.

 

 

 

이제 없는 ID로 조회하여 예외를 한번 발생시켜 보겠습니다.

 

 

XrayService의 getXray를 호출하다 예외가 발생한 걸 확인하실 수 있습니다.

 

 

예외내용은 EntityNotFoundException 이군요. XrayService의 25 Line에서 발생되었습니다.



25 Line에서 EntityNotFoundException이 발생된 걸 확인하실 수 있습니다.



이제 애플리케이션 내부에서 HTTP Client를 이용하여 다른 API를 호출해보겠습니다. 현재는 로컬에 있는 Get /xray를 호출하도록 개발했습니다.

 

 

빨간색 영역을 잘 보시면 동그라미 원에 화살표가 하나 더 감싸서 생긴 걸 볼 수있습니다. 아마 내부에서 한번 더 저 세그먼트가 생성되어서 그런걸로 보입니다.



검색창에 service("xray-application") 이라고 검색하면 현재 애플리케이션의 전체 요청내역을 볼 수 있습니다.
service 이름 괄호에 나오는 값은 위 코드에서 AWSXRayServletFilter 클래스의 생성자로 들어간 값이 service 이름입니다.

 

시간대별 검색도 가능합니다.

 

Service Lens에서 이런 정보들도 확인하실 수 있습니다.

 

 

Service별로 이런 정보들도 확인하실 수 있습니다.


보여드릴 수 있는 기능은 더 있는데 그 부분은 나중에 시간나면 정리하도록 하겠습니다.

 

참고

https://docs.aws.amazon.com/ko_kr/xray/latest/devguide/xray-api-segmentdocuments.html#api-segmentdocuments-sql
https://docs.aws.amazon.com/ko_kr/xray/latest/devguide/xray-concepts.html#xray-concepts-segments
https://docs.aws.amazon.com/ko_kr/xray/latest/devguide/xray-sdk-java-configuration.html#xray-sdk-java-configuration-envvars

https://githubplus.com/aws/aws-xray-sdk-java

https://github.com/aws/aws-xray-sdk-java/blob/master/aws-xray-recorder-sdk-sql/src/main/java/com/amazonaws/xray/sql/TracingStatement.java

https://github.com/aws/aws-xray-sdk-java/blob/master/aws-xray-recorder-sdk-sql/src/main/java/com/amazonaws/xray/sql/SqlSubsegments.java

https://docs.aws.amazon.com/ko_kr/xray/latest/devguide/xray-api-segmentdocuments.html

728x90
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함