AWS X-Ray로 실행시간 체크하기 구현(Spring)
개요
- 코드의 실행시간을 체크하기 위한 코드를 작성했었으나 간이 툴로 로그를 처리해야 결과를 알 수 있어서 불편했었다.
- 별도의 API를 만드는 방법도 있겠으나 별도의 프로젝트를 만들기도 기존 코드에 API를 추가하기도 망설여졌다.
- 프로젝트 초기부터 AWS X-Ray를 도입하자는 이야기는 있었으나 우선순위에 밀려 대응하지 않다가 이제서야 대응할 수 있었다.
- X-Ray에 대한 지식도 없고 적용하는 방법도 하나가 아니라서 이것저것 알아보다가 다른 사람의 시행착오를 줄이고자 정리한다.
AWS X-Ray에 대하여
기본 사항
- 공짜는 아니다. 매달 10만건 까지는 무료이고 초과하면 1백만건당 5USD이다.(2023/08/02 기준)
https://aws.amazon.com/ko/xray/pricing/ - 모든 리퀘스트에 대해 추적하지는 않는다. 1초안에 들어오는 리퀘스트에서 일정 건수.
일정 건수를 넘긴 시점에서는 확률에 의해 추적한다. 자세한 내용은 샘플링 규칙을 참고바란다. - 검색 범위는 6시간이다.
(정확한 사항은 아니나, 브라우저에서 검색하면 최대 6시간까지 설정할 수 있다.)
구성과 구성요소 설명

- Application code:
우리의 코드이다 - X-Ray SDK:
우리의 코드에 조합하여 X-Ray daemon에게 데이터를 보내는 AWS의 라이브러리이다. - X-Ray daemon:
데이터를 모았다가 AWS X-Ray API에 보내는 서비스이다. - X-Ray API:
AWS의 X-Ray 서비스이다. - X-Ray console:
AWS에 접속한 페이지이다. 여기서 결과를 조회한다.
우리에게 필요한 구성요소만 설명하였다.
세그먼트에 대하여
X-Ray에서 처리하는 데이터의 기본단위이다.
세그먼트는 (그냥)세그먼트와 서브 세크먼트로 나뉘는데 이름으로 알 수 있듯이 서브 세그먼트는 세그먼트의 아래에 위치한다.
쉽게 이해하기 위해서 실제로 주고받는 데이터를 확인해보자.
아래 화면은 AWS에서 제공하는 X-Ray의 콘솔화면이다. 오른쪽에 보면 미가공 데이터라는 버튼이 보이는데 이걸 누르면 주고 받았을 것으로 예상되는 데이터를 확인할 수 있다.

가장 상위에 "Segments"라는 키가 있고 데이터는 배열 형식이다.
배열 안에 들어가는 데이터가 segment일 것이다. segment에는 Document.subsegments키가 있는데 이 안의 데이터가 서브 세그먼트일 것이다.
세그먼트와 서브 세그먼트를 잘 설정해서 보내면 해석은 AWS에서 해주고 아래와 같이 그래프도 보여줄 것이다.
세그먼트의 Document.name "sample_xray"가 표시되고, 서브 세그먼트의 name인 "MainController#main1", "SubService#sub2_2"도 표시되는 것을 알 수 있다.

세그먼트와 서브 세그먼트만 잘 끼워 넣을 수 있으면 원하는 그래프도 그릴 수 있을 것이다.
X-Ray 데몬에 대하여
X-Ray SDK와 X-Ray API사이의 중간 단계이다.
SDK에서는 직접 AWS에 데이터를 보내지 않고 이 데몬으로 트레이스 데이터를 보낸다.
그러면 이 데몬에서 데이터를 모아서 X-Ray API로 보낸다.
이 데몬에 데이터를 보낼때는 특별한 인증은 필요 없는 것 같다.
데몬을 기동할 때는 X-Ray API에 대한 인증 정보가 필요하다.(액세스 키, 시크릿)
샘플링 규칙에 대하여
샘플링이란 이 세그먼트가 샘플인지 아닌지 판단하는 작업이다.
샘플은 X-Ray 데몬에 보내지고 샘플이 아니면 보내지 않는다.
이 샘플링 작업의 규칙도 만들 수 있는데, 디폴트 값이 있고 host, http method, url별로 설정할 수도 있다.
- 설정값에 대해
- url_path
- 말 그대로 적용할 URL이다. context path를 설정하였다면 context path도 함께 설정하여야 한다.
- fixed_target
- 1초안에 들어오는 요청에 대해 무조건 샘플로 처리할 숫자이다.
- 예) 5로 설정하고 1초안에 10개의 요청이 들어오면 앞에서 5개는 샘플이다
- 예) 3으로 설정하고 1초안에 10개의 요청이 들어오면 앞에서 3개는 샘플이다
- AWS문서에서는 Reservoir으로 부르는 것 같다.
- 1초안에 들어오는 요청에 대해 무조건 샘플로 처리할 숫자이다.
- rate
- fixed_target이후의 요청이 샘플이 되는 비율이다
- 예) fixed_target이 5이고 rate가 0.1일때 10개의 요청이 들어오면 앞에서 5개는 샘플이 되고
5개 이후의 요청은 10%(0.1)의 확률로 샘플이 된다.
적어도 5개 최대 10개의 샘플이 발생한다.
- 예) fixed_target이 5이고 rate가 0.1일때 10개의 요청이 들어오면 앞에서 5개는 샘플이 되고
- fixed_target이후의 요청이 샘플이 되는 비율이다
- url_path
{
"version": 2,
"rules": [
{
"description": "main1",
"host": "*",
"http_method": "*",
"url_path": "/sample-xray/main1",
"fixed_target": 5,
"rate": 0.01
}
],
"default": {
"fixed_target": 3,
"rate": 0.1
}
}
구현하기
X-Ray용 AWS계정 만들기

- AWS X-Ray API에 통신할 때 필요한 계정을 만들어주자.
- AWSXrayFullAccess권한을 주고 액세스 키도 만들어주자.
X-Ray 데몬 도커 설치하기
- docker-compose를 이용하였다.
- docker-compose.yaml 내용
- command에 --local-mode를 추가해줬는데 "EC2인스턴스 메타데이터는 선택하지 마십시오."라는 옵션이다.
설명만으로는 정확히 모르겠지만 EC2가 아니라 로컬이라 굳이 넣어줬다. - --log-file 옵션과 volumes설정은 로그를 콘솔이 아닌 파일로 빼기 위한 설정이라 없어도 무방하다.
- environment만 잘 설정하면 움직일 것으로 보인다.
- 기동에 성공하면 아래와 같은 로그가 보인다.
2023-08-01T05:17:14Z [Info] Initializing AWS X-Ray daemon 3.3.7
2023-08-01T05:17:14Z [Info] Using buffer memory limit of 78 MB
2023-08-01T05:17:14Z [Info] 1248 segment buffers allocated
2023-08-01T05:17:14Z [Info] Using region: ap-northeast-1
2023-08-01T05:17:14Z [Info] HTTP Proxy server using X-Ray Endpoint : https://xray.ap-northeast-1.amazonaws.com
2023-08-01T05:17:14Z [Info] Starting proxy http server on 0.0.0.0:2000
- 좀더 자세한 내용은 공식 사이트를 참조하자.
https://docs.aws.amazon.com/ko_kr/xray/latest/devguide/xray-daemon.html
sampleForXray프로젝트 작성
- 업무용 프로젝트에서 시험하면 정확한 의존관계를 파악하기 힘들어서 새로운 프로젝트를 만들었다.
- 환경
- Spring boot 2.7.9
- Java 11(corretto)
- source: https://github.com/MinseongNa/sampleForXray
- 환경
- 목적이 업무용 프로젝트에 적용하기 위한 것이라 업무용 프로젝트와 같은 환경으로 진행하였다.
- 원하는 기능
- 컨트롤러
- 1개만 있어도 무방
- 메인 서비스
- 컨트롤러가 부를 메소드 1개 이상
- 서브 서비스
- 메인 서비스가 부를 서비스
- 내부에서 같은 클래스 내의 메소드를 부를 것
- 처리 속도가 너무 빠르면 그래프가 꼬이는 문제가 생길 수 있어서 메소드 안에서는 지연처리할 것.
- 컨트롤러
- 실제 코드
컨트롤러
@GetMapping("/main1")
public String main1() {
return this.mainService.main1();
}
메인 서비스
public String main1() {
this.subService.sub1();
this.main2();
this.subService.sub2();
this.main3();
return String.format("{\"timeStamp:\":\"%s\"}", LocalDateTime.now());
}
private void main2() {
SleepUtil.sleep(100);
}
private void main3() {
SleepUtil.sleep(100);
}
서브 서비스
public void sub1() {
SleepUtil.sleep(100);
}
public void sub2() {
SleepUtil.sleep(100);
this.sub2_1();
this.sub2_2();
this.sub2_3();
}
private void sub2_1() {
SleepUtil.sleep(100);
}
private void sub2_2() {
SleepUtil.sleep(100);
}
private void sub2_3() {
SleepUtil.sleep(100);
}
sampleForXray프로젝트에 AWS X-Ray설정
- 필요 의존관계 설정
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-xray-recorder-sdk-bom</artifactId>
<version>2.14.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-xray-recorder-sdk-core</artifactId>
</dependency>
</dependencies>
- X-Ray GlobalRecorder설정, AWSXRayServletFilter설정
- X-Ray GlobalRecorder설정
- 각 URL별 샘플링 규칙 설정을 위해 withSamplingStrategy를 설정하였다.
(당장은 필요없지만 나중에 추가보다는 지금 넣어두는 게 나으니까.)
- 각 URL별 샘플링 규칙 설정을 위해 withSamplingStrategy를 설정하였다.
- AWSXRayServletFilter설정
- 기본 세그먼트를 만들어주고 Request로 필요한 정보도 설정해준다.
- Order는 적어도 세그먼트 처리를 하는 다른 필터보다 앞으로 한다.
- X-Ray GlobalRecorder설정
@Configuration
public class AwsXrayConfiguration {
static {
AWSXRayRecorderBuilder builder = AWSXRayRecorderBuilder.standard();
builder.withSamplingStrategy(
new LocalizedSamplingStrategy(AwsXrayConfiguration.class.getResource("/sampling-rules.json")));
AWSXRay.setGlobalRecorder(builder.build());
}
@Bean
@Order(OrderFilter.AWS_XRAY_SERVLET_FILTER)
public Filter TracingFilter() {
return new AWSXRayServletFilter("sample_xray");
}
}
public class OrderFilter {
public static final int AWS_XRAY_SERVLET_FILTER = 0;
public static final int XRAY_LOGGING_FILTER = 1;
}
- XrayLoggingFilter작성
- 원래는 로그에 관한 부분도 처리하려고 했었기에 이름이 XrayLoggingFilter가 되었지만 더 적당한 다른 이름이 있을 것 같다. 세그먼트 핸들러라던가...
- Controller와 Service의 모든 메소드가 실행될 때 동작한다.
이 때 문제가 있는데 같은 클래스의 메소드를 부르거나 private인 메소드를 부르면 동작하지 않는다.
이에 대해서도 밑에서 대응하였다. - 기능은 단순히 기본 세그먼트(AWSXRayServletFilter가 만든)에 서브 세그먼트를 추가하는 것이다.
만약에 기본 세그먼트가 없으면 에러를 뱉어낼 것이다. - 원래 원하는 기능은 같은 레벨이라면 상위 세그먼트의 서브 세그먼트로 추가되고,
하위 레벨이라면 현재 세그먼트의 서브 세그먼트로 추가하고 싶었지만
아직 좋은 생각이 나오지 않아서 단순히 처리하였다. - 세그먼트 이름은 {클래스명}#{메소드명} 으로 처리하였다.
@Aspect
@Component
@Order(OrderFilter.XRAY_LOGGING_FILTER)
public class XrayLoggingFilter {
@Around(value = "execution(* com.example.xray..*Controller.*(..))" +
" || execution(* com.example.xray..*Service.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
var globalRecorder = AWSXRay.getGlobalRecorder();
var segmentName = this.getSegmentName(proceedingJoinPoint);
var subsegment = globalRecorder.beginSubsegment(segmentName);
Objects.requireNonNull(globalRecorder.getCurrentSegment())
.addSubsegment(subsegment);
var result = proceedingJoinPoint.proceed();
globalRecorder.endSubsegment(subsegment);
return result;
}
private String getSegmentName(ProceedingJoinPoint proceedingJoinPoint) {
var classNameWithPackage = proceedingJoinPoint.getTarget().getClass().toString();
var className = classNameWithPackage.substring(classNameWithPackage.lastIndexOf(".") + 1);
var methodName = proceedingJoinPoint.getSignature().getName();
return String.format("%s#%s", className, methodName);
}
}
AspectJ로 컴파일하도록 변경하기
2023/08/08 추가사항
AspectJ로 컴파일을 하면 lombok과 충돌이 발생하는 것을 확인하였다.
사용한 lombok기능은 @Slf4j인데, AspectJ와 마찬가지로 코드를 덧붙여주기때문에 충돌이 난 것으로 보인다.
해결 방법이 아예 없는 것은 아니고 롬복의 처리가 끝나고 위빙을 하면 된다고 한다.
https://plugin.jcabi.com/example-ajc.html
아직 시도해보지는 않았다.
아래는 메이븐의 에러 화면이다.

위에까지의 코드는 스프링AOP를 이용하기 때문에 같은 클래스의 메소드나 private에 대해서 처리할 수 없다.
스프링AOP의 한계라서 스프링AOP를 쓰면서 해결한 방법은 없다고 한다.
그래서 아래 그림처럼 main2, main3, sub2_1,sub2_2,sub2_3의 정보가 누락된 그래프가 표시되어버린다.

해결 방법은 AspectJ를 이용해 컴파일 위빙으로 바이트 코드를 만들면 해결된다고 한다. 속도도 더 빨라진다고 하니 바로 바꿔보았다.
- 필요 의존관계 설정
- aspectj 버전이 1.9.7보다 높으면 property를 이용해 설정하길 권장하고 있다.
- plugin에서 <complianceLevel>을 따로 설정해주었는데, 이 설정이 없으면 어노테이션 관련 에러가 발생하여 추가하였다.
<properties>
<aspectj.version>1.9.19</aspectj.version>
</properties>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>${aspectj.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectj.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.14.0</version>
<configuration>
<complianceLevel>11</complianceLevel>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
위와 같이 설정하고 메이븐으로 빌드하면 AspectJ로 컴파일된 jar를 생성할 수 있다.
인텔리제이에서 컴파일 하는 경우는 아래와 같이 Ajc로 컴파일러를 바꾸면 인텔리제이에서 빌드시 AspectJ를 통해 컴파일이 된다.

AspectJ로 컴파일된 경우에는 누락되는 메소드 없이 아래와 같은 결과를 얻을 수 있다.

AWS X-Ray API에서 기록보기
CloudWatch에서 "X-Ray 기록" - "서비스 맵" 을 누르고 시간 범위를 조정하면 아래와 같은 화면이 나온다.
세그먼트의 name이 sample_xray하나 뿐이라서 표시되는 종류가 하나인데,
여러 서비스에서 데이터를 보낸다면 더 많은 내용이 표시될 것이다.

sample_xray를 누르면 좀 더 상세한 화면이 나온다.

CloudWatch에서 "X-Ray 기록" - "기록"을 누르고 시간 범위를 조정하면 아래와 같은 화면이 나온다.

기록중에 하나를 누르면 아래와 같은 화면이 나온다.
로그에 대한 부분도 있는데, 현재 상황에서는 개인계정의 X-Ray만 이용하고 CloudWatch에 로그는 남기지 않아서 연결된 로그를 확인할 수 없다.
로그에 대해서도 대응하려면 아래 링크를 참조하면 대응할 수 있을 것이다.

그외
LocalStack Pro를 이용하여 로컬에서 X-Ray이용
S3는 LocalStack을 이용하고 있어서 X-Ray도 있지 않을까 찾아봤는데 Pro버전에서는 지원하고 있었다.
이미 LocalStack Pro 이상을 이용하고 있다면 좀 더 다양한 시도가 가능할 것 같다.
https://localstack.cloud/pricing/
https://docs.localstack.cloud/user-guide/aws/feature-coverage/
AWS X-Ray를 적용하는 다른 방법 - javaAgent
이번 대응에서는 SDK로 직접 세그먼트를 설정하였지만, 자바 에이전트를 통해서 자동으로 계측하는 방법도 있다.
"AWSDistro for OpenTelemetry Java"은 시도하지 않았고 "disco"라고 하는 에이전트는 간단하길래 사용해봤었는데,
계측 자체는 잘 되었지만 아래 사진에서처럼(대응했던 결과는 아니고 AWS의 예시 사진이다.)
어느 기능(DynamoDB, S3 등)을 사용하는지는 보이지만 그게 내 어떤 메소드인지는 표시되지 않아서 쓰지 않았었다.
만약에 니즈가 맞다면 disco를 먼저 써보는 것이 좋을 듯 하다.

문제 요청 샘플링하기
직접 세그먼트를 제어하면 샘플링 규칙과 관계없이 세그먼트를 샘플로 정하는 것이 가능하다.
예를 들면 평상시에 100ms의 응답속도인데 가끔 3000ms를 넘는 요청이 발생한다면
세그먼트에 setSampled(true)하여 샘플로 지정이 가능하다.
참고자료
AWS X-Ray관련
AspectJ관련