AWS X-Ray
AWS X-Ray는 애플리케이션을 추적하고 분석하는 데 도움을 주는 서비스이다. 이를 통해 애플리케이션의 성능 문제를 식별하고, 애플리케이션의 동작 방식을 더 잘 이해할 수 있다. X-Ray는 애플리케이션의 요청을 추적하고, 각 요청이 어떻게 처리되는지에 대한 자세한 정보를 제공한다.
주요 기능
- 트레이스 수집 및 분석: 요청이 애플리케이션을 통해 어떻게 이동하는지 시각화한다.
- 트랜잭션 오류 및 성능 병목 현상 식별: 어디에서 오류가 발생하고 지연이 발생하는지 파악할 수 있다.
- 종단 간 뷰 제공: 프런트엔드 서비스에서 백엔드 서비스까지 전체 애플리케이션 스택을 추적할 수 있다.
구성요소
- 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 |