DevOps/AWS

X-Ray

kyoulho 2024. 7. 16. 08:54

AWS X-Ray


AWS X-Ray는 애플리케이션을 추적하고 분석하는 데 도움을 주는 서비스이다. 이를 통해 애플리케이션의 성능 문제를 식별하고, 애플리케이션의 동작 방식을 더 잘 이해할 수 있다. X-Ray는 애플리케이션의 요청을 추적하고, 각 요청이 어떻게 처리되는지에 대한 자세한 정보를 제공한다.

주요 기능

  1. 트레이스 수집 및 분석: 요청이 애플리케이션을 통해 어떻게 이동하는지 시각화한다.
  2. 트랜잭션 오류 및 성능 병목 현상 식별: 어디에서 오류가 발생하고 지연이 발생하는지 파악할 수 있다.
  3. 종단 간 뷰 제공: 프런트엔드 서비스에서 백엔드 서비스까지 전체 애플리케이션 스택을 추적할 수 있다.

구성요소

  • Segment: 애플리케이션의 개별 작업을 나타낸다.
  • Subsegment: 세그먼트 내의 더 작은 작업을 나타내어 더 세부적인 추적을 가능하게 한다.
  • Trace: 여러 세그먼트가 결합되어 전체 요청의 경로를 나타낸다.
  • Annotations: 검색 및 필터링에 사용되는 키-값이다.
  • Metadata: 더 자세한 정보를 포함하며, 검색이나 필터링에는 사용되지 않는다.

작동 원리

  • 서비스 인스트루멘테이션 (Service Instrumentation):
    • AWS X-Ray SDK를 사용하여 애플리케이션 코드를 추가한다.
    • SDK는 다양한 프레임워크와 런타임 환경에서 지원되어, Java, Kotlin, Python, Node.js 등 다양한 언어와 플랫폼에서 사용할 수 있다.
  • 샘플링 결정 (Sampling Decision):
    • 모든 요청을 추적하는 것은 리소스 소모가 크기 때문에, X-Ray은 샘플링을 통해 일부 요청만을 추적한다.
    • 샘플링 규칙은 어떤 요청이 추적될지 결정하는 기준이 된다.
    • 예를 들어, 고정된 트랜잭션 수, 비율 기반 샘플링 등 다양한 방식으로 설정할 수 있다.
  • 세그먼트와 서브세그먼트 생성:
    • 각 요청은 세그먼트(Segment)로 표시된다.
    • 세그먼트는 전체 요청의 상위 수준 정보를 제공하며, 요청의 시작과 끝 사이에서 발생한 서브세그먼트(Subsegment)들을 포함할 수 있다.
    • 서브세그먼트는 세그먼트 내에서 개별적인 작업 단위를 나타내며, 세분화된 추적 정보를 제공한다.
    • 예를 들어, HTTP 요청 처리, 데이터베이스 쿼리 실행, 외부 API 호출 등이 서브세그먼트로 기록될 수 있다.
  • 메타데이터 및 흐름 정보 수집:
    • 각 세그먼트와 서브세그먼트에는 추가적인 메타데이터와 흐름 정보(Annotations, Metadata)가 포함될 수 있다.
    • 이 정보는 추적된 작업의 상세한 콘텍스트를 제공하며, 사용자가 필요에 따라 추가 데이터를 기록할 수 있는 장점을 제공한다.
    • 예를 들어, HTTP 요청의 헤더 정보, 사용자 ID, 처리된 데이터의 크기 등을 메타데이터로 기록할 수 있다.
  • 분석 및 시각화:
    • AWS X-Ray 콘솔을 통해 추적된 데이터를 시각적으로 탐색하고 분석할 수 있다.
    • X-Ray 콘솔은 각 요청의 전체 경로와 각 단계에서 발생한 지연, 오류 등을 시각적으로 제공하여 문제 해결과 성능 최적화에 도움을 준다.
    • 서브세그먼트의 실행 시간, 응답 코드, 오류 상태 등의 데이터를 기반으로 성능 이슈를 식별하고, 서비스 간의 의존성을 시각화하여 병목 현상을 해결할 수 있다.

AWS X-Ray Daemon

X-Ray Daemon은 애플리케이션과 AWS X-Ray 서비스 간의 중간자 역할을 수행하며, 추적 데이터를 수집하고 전송하여 분산된 애플리케이션의 성능을 분석하는 데 중요한 역할을 한다.

  • 추적 데이터 수집:
    • X-Ray SDK가 생성한 세그먼트와 서브세그먼트를 수집한다.
  • 데이터 버퍼링:
    • 수집된 데이터를 일시적으로 버퍼링하여 효율적으로 전송할 수 있도록 한다.
  • 전송 최적화:
    • 일정량의 데이터를 모아서 AWS X-Ray로 전송하여 네트워크 사용을 최적화한다.
  • UDP 포트 사용:
    • 애플리케이션과의 통신은 UDP 프로토콜을 사용한다. 기본적으로 포트 2000을 사용하지만, 필요에 따라 다른 포트로 변경할 수 있다.

Daemon이 AWS X-Ray 서비스에 추적 데이터를 전송하려면, AWS에서 제공하는 AWSXRayDaemonWriteAccess 역할을 서버(예: EC2 인스턴스)에 부여해야 한다. 이 역할은 AWS X-Ray Daemon이 AWS X-Ray 서비스로 데이터를 전송할 수 있도록 필요한 권한을 제공한다. AWS는 각 OS 패키지 매니저, 도커 이미지 등을 제공하고 있다.

 

docker-componse.yml

version: '3.8'

services:
  app:
    image: your-application-image:latest
    container_name: app
    ports:
      - "8080:8080"
    environment:
      - AWS_XRAY_DAEMON_ADDRESS=xray-daemon:2000
      # 필요한 다른 환경 변수들 추가
    depends_on:
      - xray-daemon

  xray-daemon:
    image: amazon/aws-xray-daemon:latest
    container_name: xray-daemon
    ports:
      - "2000:2000/udp"
    volumes:
      - ~/.aws:/root/.aws:ro
    environment:
      - AWS_REGION=your-aws-region

 

Gradle Kotlin DSL

dependencyManagement {
    imports {
        mavenBom('com.amazonaws:aws-xray-recorder-sdk-bom:2.10.0')
    }
}

dependencies {
    // AWS X-Ray의 핵심 SDK 모듈로, X-Ray 추적을 위한 기본적인 기능을 제공한다.
    // 세그먼트 생성 및 전송을 위해 필요하며 사용자의 요청을 받을 때
    // 세그먼트를 구성하는 AWSXrayServletFilter가 있다
    compile("com.amazonaws:aws-xray-recorder-sdk-core")

    // Apache HTTP Client를 위한 AWS X-Ray SDK 모듈
    // 애플리케이션이 다른 HTTP API를 호출할 때 해당 요청을 추적한다.
    compile("com.amazonaws:aws-xray-recorder-sdk-apache-http")

    // AWS SDK 클라이언트와 통합하여 AWS 서비스를 호출할 때 사용된다.
    // 이 모듈은 AWS 서비스와의 상호 작용을 추적하고, 각 AWS 서비스 호출의 성능을 분석할 수 있도록 도와준다.
    compile("com.amazonaws:aws-xray-recorder-sdk-aws-sdk")

    // 데이터베이스에 요청한 SQL 수행 시간 및 DBMS에 대한 정보를 추적하는 모듈
    compile("com.amazonaws:aws-xray-recorder-sdk-sql")

    // 로그 메시지에 X-Ray 트레이스 ID를 주입하는 모듈
    // 이를 통해 로그에서 추적 ID를 확인할 수 있어, 로그와 X-Ray 추적 데이터를 연결할 수 있다.
    compile("com.amazonaws:aws-xray-recorder-sdk-slf4j")
}

 

샘플링 규칙

{
  "version": 2,
  "rules": [
    {
      "host": "*",          // 규칙을 적용할 호스트 이름
      "http_method": "*",   // 적용될 HTTP Method
      "url_path": "/api/*", // 적용될 URL
      "fixed_target": 1,    // 샘플링 할 초당 트레이스 수. 0으로 세팅시 데이터 수집하지 않음
      "rate": 0.01          // fixed_target에 도달한 후 규칙이 샘플링해야 하는 비율, 1이 100%
    },
    {
      "host": "*",
      "http_method": "GET",
      "url_path": "/public/*",
      "fixed_target": 2,
      "rate": 0.05
    }
  ],
  "default": {
    "fixed_target": 0,      // 기본적으로 샘플링을 비활성화
    "rate": 0.0             // 기본 샘플링 비율을 0%로 설정하여 모든 요청을 추적하지 않는다.
  }
}

 

logback-spring.xml

로그 패턴에 트레이스 아이디를 넣어주면 CloudWatch Log에서 해당 트레이스 ID를 주입시켜 주며 트레이스 아이디로 특정 요청 및 응답에 대한 정보를 볼 수 있다. AWS Log Appender 라이브러리와 여러 설정이 필요하지만 여기서 다루지 않겠다.

<configuration>

    <!-- AWS X-Ray Trace ID를 로그에 주입하는 패턴 설정 -->
    <conversionRule conversionWord="AWS-XRAY-TRACE-ID" converterClass="com.amazonaws.xray.logback.TracingHeaderConverter" />

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 로그 패턴 설정 -->
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg %X{AWS-XRAY-TRACE-ID}%n</pattern>
        </encoder>
    </appender>

    <!-- 루트 로거 설정 -->
    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>

</configuration>

 

application.yml

aws:
  xray:
    # AWS X-Ray에서 사용할 고정된 세그먼트 이름을 설정한다.
    # 이 이름은 애플리케이션의 전반적인 트레이스를 그룹화하는 데 사용된다.
    fixed-segment-name: xray-application

    # X-Ray에서 생성된 로그 이름의 접두사를 설정한다.
    # 예를 들어, AWS-XRAY로 시작하는 모든 로그 이름이 생성될 수 있다.
    prefix-log-name: AWS-XRAY
   
    # 샘플링 규칙을 정의한 JSON 파일의 경로를 지정한다.
    # 이 파일은 어플리케이션의 요청을 샘플링하여 AWS X-Ray에 전송할 요청을 결정하는 데 사용된다.
    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

 

구성 클래스

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

@Configuration
class XRaySegmentConfig {

    @Value("\${aws.xray.fixed-segment-name}")
    private lateinit var fixedSegmentName: String

    @Value("\${aws.xray.prefix-log-name}")
    private lateinit var prefixLogName: String

    @Value("\${aws.xray.sampling-rules-json}")
    private lateinit var samplingRulesJson: String

    @PostConstruct
    fun init() {
        // 고정 세그먼트 이름으로 세그먼트를 시작
        AWSXRay.beginSegment(fixedSegmentName)

        val ruleFileUrl = javaClass.getResource(samplingRulesJson)
        val builder = AWSXRayRecorderBuilder.standard().apply {
            // EC2 플러그인을 추가
            withPlugin(EC2Plugin())
            // 로컬 샘플링 전략을 사용하여 샘플링 규칙 파일을 설정
            withSamplingStrategy(LocalizedSamplingStrategy(ruleFileUrl))
            // SLF4J를 사용하여 세그먼트 리스너를 설정
            withSegmentListener(SLF4JSegmentListener(prefixLogName))
            // 컨텍스트 누락 전략을 설정
            withContextMissingStrategy(IgnoreContextMissingStrategy())
        }
        // 글로벌 레코더 설정
        AWSXRay.setGlobalRecorder(builder.build())

        // 세그먼트를 종료
        AWSXRay.endSegment()
    }

    @Bean
    fun tracingFilter(): Filter = AWSXRayServletFilter(fixedSegmentName)

    // 컨텍스트가 누락되었을 때 아무 동작도 수행하지 않는 전략을 정의
    private class IgnoreContextMissingStrategy : ContextMissingStrategy {
        override fun contextMissing(message: String?, exceptionClass: Class<out RuntimeException>?) {
            // 컨텍스트가 누락되었을 때 아무 동작도 수행하지 않음
        }
    }

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

애플리케이션이 AWS X-Ray와 통합되어 데이터베이스 작업의 성능과 세부 정보를 추적하려면 데이터베이스 요청 시 하위 세그먼트(Subsegment)가 생성되도록 TracingDataSource로 데이터 소스를 래핑해야 합니다. 이를 통해 각 데이터베이스 요청이 트레이싱되며, SQL 수행 시간 및 DBMS 관련 정보를 AWS X-Ray 콘솔에서 확인할 수 있다.

IgnoreContextMissingStrategy 설정은 애플리케이션 구동 시 발생할 수 있는 예외, 즉 X-Ray 세그먼트가 생성되기 전에 하위 세그먼트를 생성하려는 시도를 방지하기 위한 것이다. 이를 위해 데이터 소스를 구성하고 트레이싱 기능을 추가해야 합니다.

 

HTTP 클라이언트 설정

 

RestTemplate, FeignClient등과 같은 HTTP 클라이언트에 사용할 수 있다.

import com.amazonaws.xray.proxies.apache.http.HttpClientBuilder
import feign.Client
import feign.Logger
import feign.Request
import feign.RequestInterceptor
import feign.codec.ErrorDecoder
import org.apache.http.impl.client.CloseableHttpClient
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager
import org.springframework.cloud.openfeign.EnableFeignClients
import org.springframework.cloud.openfeign.FeignClient
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
@EnableFeignClients
class XRayFeignConfig {

    companion object {
        // 각 라우트당 최대 연결 수를 200으로 설정
        private const val DEFAULT_MAX_PER_ROUTE = 200

        // 전체 최대 연결 수를 50으로 설정
        private const val MAX_TOTAL = 50

        // 연결 타임아웃을 5000밀리초로 설정
        private const val CONNECT_TIMEOUT_MILLIS = 5000

        // 읽기 타임아웃을 5000밀리초로 설정
        private const val READ_TIMEOUT_MILLIS = 5000
    }

    @Bean
    fun feignClient(): Client {
        val manager = PoolingHttpClientConnectionManager().apply {
            defaultMaxPerRoute = DEFAULT_MAX_PER_ROUTE
            maxTotal = MAX_TOTAL
        }

        // AWS X-Ray와 통합된 HttpClientBuilder를 사용하여 HttpClient를 생성
        val httpClient: CloseableHttpClient = HttpClientBuilder.create()
            .setConnectionManager(manager)
            .build()

        // Feign 클라이언트에서 사용할 기본 HTTP 클라이언트를 반환
        return Client.Default(httpClient, null)
    }

    @Bean
    fun requestInterceptor(): RequestInterceptor {
        return RequestInterceptor { template ->
            // 각 요청에 현재 트레이스 ID를 헤더로 추가
            template.header("X-Amzn-Trace-Id", AWSXRay.getCurrentSegment().traceId.toString())
        }
    }

    @Bean
    fun feignLoggerLevel(): Logger.Level {
        // 모든 요청 및 응답 세부 정보를 로깅하도록 설정
        return Logger.Level.FULL
    }

    @Bean
    fun feignErrorDecoder(): ErrorDecoder {
        // 기본 에러 디코더를 사용
        return ErrorDecoder.Default()
    }

    @Bean
    fun requestOptions(): Request.Options {
        // 연결 및 읽기 타임아웃을 설정
        return Request.Options(CONNECT_TIMEOUT_MILLIS, READ_TIMEOUT_MILLIS)
    }
}
@FeignClient(name = "myFeignClient", url = "\${my.feign.client.url}", configuration = [XRayFeignConfig::class])
interface MyFeignClient {
    @GetMapping("/endpoint")
    fun getSomething(): ResponseEntity<String>
}

 

SQL 트레이싱

AWS X-Ray는 다양한 JDBC 드라이버와 연동하여 SQL 쿼리를 추적할 수 있다. 만약 Tomcat Jdbc Pool을 사용하는 경우에도 설정을 통해 SQL 쿼리를 추적할 수 있다. 

# JVM 옵션
-Dcom.amazonaws.xray.collectSqlQueries=true
# OS 환경 변수 설정
export AWS_XRAY_COLLECT_SQL_QUERIES=true

 

이 설정은 Java 시스템 속성 및 OS 환경 변수 중 하나를 선택하여 설정하면 된다. 둘 다 설정할 필요는 없다.

위 설정을 사용하면 AWS X-Ray가 실행된 모든 SQL 쿼리를 추적하게 된다

 

 

Spring AOP로 세그먼트 생성하기

Spring Boot 애플리케이션에서 X-Ray 추적을 활성화하려면 Spring AOP를 사용하여 메서드를 X-Ray 인스트루멘테이션으로 감싸야한다. 여기에는 두 가지 접근 방식이 있다

 

1. AbstractXRayInterceptor

import com.amazonaws.xray.AWSXRay
import com.amazonaws.xray.entities.Subsegment
import com.amazonaws.xray.spring.aop.AbstractXRayInterceptor
import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Pointcut
import org.springframework.stereotype.Component
import org.springframework.util.ObjectUtils
import java.lang.reflect.Parameter

@Aspect
@Component
class XRayInterceptor : AbstractXRayInterceptor() {

    // 추적할 클래스와 메서드를 정의하는 포인트컷
    @Pointcut("@within(com.amazonaws.xray.spring.aop.XRayEnabled) && bean(*Controller)")
    override fun xrayEnabledClasses() {
        // 빈 메서드는 기본적인 추적 대상 설정을 사용하겠다는 의미이며,
        // 따로 추가적인 클래스나 메서드 지정이 필요하지 않다는 것을 나타낸다.
    }
	
    // 추적할 메서드 실행 시 생성할 메타데이터를 정의
    override fun generateMetadata(proceedingJoinPoint: ProceedingJoinPoint, subsegment: Subsegment): Map<String, Map<String, Any>> {
        val metadata = mutableMapOf<String, Map<String, Any>>()

        val parameters = proceedingJoinPoint.signature.methodParameters
        val args = proceedingJoinPoint.args

        val requestMetadata = mutableMapOf<String, Any>()
        for (i in parameters.indices) {
            requestMetadata[parameters[i].name ?: "arg$i"] = ObjectUtils.nullSafeToString(args[i])
        }
        metadata["request"] = requestMetadata

        return metadata
    }
}

 

2. BaseAbstractXRayInterceptor

import com.amazonaws.xray.AWSXRay
import com.amazonaws.xray.entities.Subsegment
import com.amazonaws.xray.spring.aop.BaseAbstractXRayInterceptor
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.springframework.stereotype.Component
import org.springframework.util.ObjectUtils
import java.lang.reflect.Parameter

@Aspect
@Component
class XRayInterceptor : BaseAbstractXRayInterceptor() {

    
    // 추적할 클래스와 메서드를 정의하는 포인트컷
    @Pointcut("@within(com.amazonaws.xray.spring.aop.XRayEnabled) && bean(*Controller)")
    override fun xrayEnabledClasses() {}

   
    // 메서드 호출 주변에 X-Ray 추적을 적용하는 어라운드 어드바이스
    @Around("xrayEnabledClasses()")
    override fun aroundXRayTrace(joinPoint: ProceedingJoinPoint): Any? {
        val subsegment = AWSXRay.beginSubsegment(getSubsegmentName(joinPoint))

        try {
            val metadata = generateMetadata(joinPoint, subsegment)
            subsegment.putMetadata("request", metadata["request"])

            return joinPoint.proceed()
        } catch (throwable: Throwable) {
            AWSXRay.getCurrentSubsegmentOptional().ifPresent { currentSubsegment ->
                currentSubsegment.addException(throwable)
            }
            throw throwable
        } finally {
            AWSXRay.endSubsegment()
        }
    }


    // 메서드 호출 시 생성할 메타데이터를 정의
    private fun generateMetadata(proceedingJoinPoint: ProceedingJoinPoint, subsegment: Subsegment): Map<String, Map<String, Any>> {
        val metadata = mutableMapOf<String, Map<String, Any>>()

        val parameters = proceedingJoinPoint.signature.methodParameters
        val args = proceedingJoinPoint.args

        val requestMetadata = mutableMapOf<String, Any>()
        for (i in parameters.indices) {
            requestMetadata[parameters[i].name ?: "arg$i"] = ObjectUtils.nullSafeToString(args[i])
        }
        metadata["request"] = requestMetadata

        return metadata
    }

    
    // Subsegment의 이름을 생성
    private fun getSubsegmentName(joinPoint: ProceedingJoinPoint): String {
        val className = joinPoint.signature.declaringType.simpleName
        val methodName = joinPoint.signature.name
        return "$className.$methodName"
    }
}
 

AbstractXRayInterceptor와 BaseAbstractXRayInterceptor 차이

  • AbstractXRayInterceptor: 추적할 클래스와 메서드를 정의하는 xrayEnabledClasses() 메서드와 추적 시 추가할 메타데이터를 생성하는 generateMetadata() 메서드를 구현해야 한다.
  • BaseAbstractXRayInterceptor: 추적할 클래스와 메서드를 정의하는 xrayEnabledClasses() 메서드를 구현해야 하고, 추적을 실행하는 aroundXRayTrace() 메서드를 구현해야 한다.

각각의 클래스는 AWS X-Ray의 추적을 위한 다른 방식을 제공하며, 사용하는 상황과 필요에 따라 선택하여 구현해야 한다. AbstractXRayInterceptor는 메서드 레벨의 추적에 대한 메타데이터 생성을 더욱 세밀하게 제어할 수 있다. 반면 BaseAbstractXRayInterceptor는 보다 간단한 추적 구현을 제공하여 사용 편의성을 높일 수 있다.

'DevOps > AWS' 카테고리의 다른 글

CloudWatch  (0) 2024.07.15
OWASP Top 10 공격 패턴  (2) 2024.07.14
Session Manger 를 이용하여 EC2에 연결하기  (0) 2023.06.18
AWS Aurora DB  (1) 2023.06.18
[WAF] NestJS 애플리케이션 AWS에 배포하기 08  (0) 2023.04.25