MSA에서 Zipkin을 사용하여 분산추적 로깅을 구현해 보자
이벤트를 사용해서 데이터베이스의 정합성 보장 문제는 해결했지만 아직 이 과정들을 전부 로그를 통해서 추적하는 것은 적용시키지 못했다. 로그가 정말 중요한 만큼 zipkin을 사용하여 제대로 로그를 추적해 보도록 하자.
이번 포스트에서는 ZIpkin을 활용하여 MSA 환경에서 분산 로그 추적을 구현하는 과정을 단계별로 설명한다.
1. ECR 리포지토리 생성하기
ECR(Elastic Container Registry)은 AWS에서 제공하는 Docker 컨테이너 이미지를 저장하기 위한 서비스다. 여기서는 Zipkin 이미지를 위한 저장소를 만드는 방법을 설명한다.
1-1. Zipkin 이미지를 저장할 AWS ECR 생성
- zipkin 용 repository를 ecr에 생성한다. 프라이빗을 선택하고 내가 원하는 리포지토리 저장소 이름을 적어준다.
- 아래로 내려가서 나머지 2개 설정은 건드리지 말고 바로 "리포지토리 생성" 버튼을 눌러준다.
1-2. 로컬에서 Dockerfile을 작성한다.
- Dockerfile에는 아래와 같이 zipkin의 dockerhub만 연결시켜 주는 코드를 작성하면 된다.
ECR에 DockerHub의 이미지를 저장하는 과정은 귀찮지만 이 작업을 꼭 해야 하는 이유가 존재한다. 이전에 CodePipeline 사용 도중 CodeBuild 단계에서 BuildSpec.yml안에 작성해 준 코드가 DockerHub를 통해서 Zipkin이미지를 받아오도록 설정했는데 어느 순간 갑자기 Zipkin 이미지에 대한 요청이 너무 많다고 에러가 발생해서 빌드가 완전히 멈췄던 적이 있었기에 이 방식을 선택했다.
FROM openzipkin/zipkin
- 로컬에서 터미널(Iterm)을 켜서 ECR에 로그인을 해준다. (첫 이미지를 local환경에서 ECR로 push하기 위함이다.)
aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin {내 aws 숫자id}.dkr.ecr.ap-northeast-2.amazonaws.com
- 우리의 EC2는 ARM 아키텍처 기반인 t4g를 사용하고 있다. 그러니 docker의 buildx를 사용해서 이미지를 arm용으로 빌드해 준다.
docker buildx build --no-cache --platform=linux/arm64 -t {내 aws 숫자id}.dkr.ecr.ap-northeast-2.amazonaws.com/{ECR 저장소 이름}:{ECR 저장소 태그} . --push
- 이미지를 넣는것에 성공하면 아래와 같이 보일 것이다.
2. NACL(네트워크 ACL) 설정하기
2-1. NACL에서도 포트를 열어줘야 하는 이유
1. NACL (Network Access Control List)
이것은 서브넷 수준에서 작동하는 상태 비저장형 필터다. NACL은 서브넷으로 들어오고 나가는 트래픽에 대한 규칙을 설정하며, 이 규칙들은 순차적으로 평가되어 트래픽을 허용하거나 거부한다. 만약 특정 트래픽이 NACL을 통과하지 못하면, 해당 트래픽은 보안 그룹에 도달조차 하지 못한다.2. 보안 그룹 (Security Group)
반면 보안 그룹은 인스턴스 수준에서 작동하는 상태 저장형 필터이다. 이것은 인스턴스로 들어오고 나가는 트래픽에 대한 규칙을 설정한다. 보안 그룹은 연결 초기에만 규칙을 평가하고, 연결이 설정되면 그 후의 트래픽은 자동적으로 허용된다.따라서, 트래픽은 먼저 NACL을 통과해야 하고, 그 후에 보안 그룹의 규칙이 적용된다. 만약 NACL에서 트래픽이 거부되면, 그 트래픽은 보안 그룹에 도달하지 못한다. 그래서 내가 NACL에서 특정 트래픽을 막으면, 그 트래픽은 보안 그룹의 규칙을 고려하기 전에 이미 차단되는 것이다.
결국 이 단계에서는 NACL을 통해 네트워크의 트래픽을 관리하며, 특정 포트를 열어 Zipkin 서버로의 통신을 가능하게 한다.
2-2. NACL메뉴 들어가기
- AWS상단의 검색창에 nacl이라고 검색해서 설정 화면으로 이동한다.
2-3. NACL을 적용시켜 줄 VPC 선택하기
- 아래와 같이 리스트가 나올 텐데 내가 Zipkin서버(ECS)를 기동한 VPC를 찾아서 클릭하여 세부 정보로 들어간다.
2-4. NACL 인바운드 규칙 편집하기
- 우측 하단에 존재하는 "인바운드 규칙 편집" 버튼을 클릭하자 이 부분에서 헷갈릴 수도 있는데 여기서 설정하는 인바운드 규칙 편집은 아까 "보안 그룹"에서 설정한 것과 다른 것이다.
- 아래와 같이 인바운드 규칙을 편집하는 화면으로 이동할 텐데 여기에서 9411번 포트를 tcp로 열어주도록 하자 (지금은 맨 상단의 규칙에 따라 모든 트래픽이 허용되지만 나중에는 보안을 위해서 모든 트래픽을 열어주는 맨 위의 규칙을 제거할 것이다. 미리 Zipkin전용 9411번 포트를 적어줘서 추후에 까먹지 않고 문제가 발생하지 않도록 작성해 준 것이다.)
2-5. NACL 인바운드 규칙 편집 성공
- 위에서 "변경 사항 저장" 버튼을 누르면 화면 상단에 아래와 같은 팝업창이 나올 것이다. 인바운드 규칙 편집에 성공한 것이다.
3. 보안 그룹 세팅하기 (NACL은 이미 모든 연결 허용인 상태)
3-1. 보안 그룹에 들어가기
- EC2에 들어가서 상단의 리소스에 적혀 있는 “보안 그룹”을 클릭해서 들어간다.
- 여기서 내가 이전에 ECS클러스터를 설정할 때 생성할 ECS전용 EC2에 보안그룹을 연결해 준 적이 있는데 그 정보를 찾아서 들어간다.
3-2. 인바운드 규칙 편집하기
- 보안그룹을 선택해서 "세부 정보" 화면으로 들어왔다면 하단의 "인바운드 규칙 편집" 버튼을 클릭한다.
- 아래와 같이 포트를 설정하는 화면이 나올 텐데 여기서 사용자 설정 TCP를 고르고 zipkin이 사용하는 9411번 포트를 열어준다.
3-3. 규칙 저장하기
- 위의 화면에서 규칙 저장 버튼을 누르면 하단과 같이 수정되었다는 팝업이 화면에 보일 것이다.
4. ECS 태스크 정의를 생성하고 ECS 서비스 만들기
참고로 지금 상황은 이미 ECS클러스터가 존재하고 있으며 이때 추가적으로 태스크 정의를 생성하는 상황이다. 만약 ECS클러스터가 없다면 아래의 글을 보고 클러스터를 생성하고 와도 좋을 것 같다.
4-1. "태스크 정의"를 진행하는 페이지로 들어가기
- ECS 클러스터 좌측의 메뉴바에 적혀있는 “태스크 정의” 버튼을 클릭한다.
4-2. 새 태스크 정의 생성하기
- 태스크 정의 화면에 들어왔으면 우측의 "새 태스크 정의 생성" 버튼을 누른다.
4-2-1. 태스크 정의 구성하기
- 먼저 태스크 정의의 패밀리 이름을 작성한다. 이건 새롭게 생성하는 태스크의 이름을 정한다고 생각하면 된다.
4-2-2. 인프라 요구 사항 작성하기
- 다음으로 시작 유형에서 EC2 인스턴스를 선택하고 운영 체제/아키텍처는 나는 t4g 인스턴스를 사용 중이므로 Linux/ARM64로 설정해 주었다. 또한 네트워크 모드는 default로 설정했다. 하단의 태스크 크기는 임의로 정해준 값이다.
4-2-3. 컨테이너 설정하기
- 다음으로 컨테이너의 세부 정보를 작성한다. (이름, 이미지 URI(나는 ERC주소 적기))
- 다음으로 포트를 매핑해 줄 것인데 프로토콜로 TCP를 선택하고 호스트:9411, 컨테이너:9411로 포트를 작성해 주면 된다.
- 환경변수는 따로 추가하지 않고 넘어간다.
- 로그 수집은 기본적으로 체크가 되어있을 것이다. 앞으로 컨테이너 로그를 볼 것이기 때문에 설정을 그대로 놔둔다.
- 나머지 사항들은 아무것도 건드리지 않고 바로 맨 하단의 "생성" 버튼을 클릭한다.
4-3. ECS 클러스터에서 방금 생성한 "태스크 정의"를 사용하여 ECS서비스를 생성한다.
- 태스크를 정의하고 그 자리에서 바로 서비스를 생성할 수도 있지만 나는 ECS클러스터 내부의 개요에 들어와서 하단의 서비스 목록에서 "생성" 버튼을 클릭해서 ECS서비스를 생성한다.
4-3-1. ECS 서비스 환경 설정하기
- 컴퓨팅 옵션으로는 "시작 유형"을 선택하고 하단에서 EC2를 선택해 준다.
4-3-2. 배포 구성하기
- 하단으로 내려와서 패밀리에 아까 만든 "태스크 정의"를 선택해 주고 ECS 이름으로 설정할 값을 "서비스 이름"에 적어준다.
4-3-3. 나머지 설정은 넘어가고 생성하기
- SpringBoot 프로젝트 전용 ECS서비스를 생성할 때는 로드밸런서를 선택해서 ALB를 등록해 줬지만 로그를 추적하기 위해 사용하는 Zipkin은 외부에서의 접속이 거의 없을 예정이라 로드밸런서를 설정하지 않고 넘어간다.
4-4. EC2로 가서 퍼블릭 DNS주소를 복사하고 뒤에 내가 설정해 준 9411번 포트를 넣고 요청을 보낸다.
- 만약 접속에 성공하면 아래와 같이 나올 것이다.
5. spring boot 설정하기
5-1. gradle 의존성 추가하기
- 지프킨과 스프링을 연결하기 위해서 SpringBoot를 설정한다. 먼저 build.gradle 파일에 의존성을 추가한다.
// zipkin
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-tracing-bridge-brave'
implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
5-2. application.yml에 설정 추가하기
- 참고로 endpoint를 작성할 때 주소 맨 뒤에 /api/v2/spans 이것을 꼭 적어줘야 한다. (이걸 적어줘야 추적이 동작함)
logging:
level:
com.recipia.member: debug
org.springframework.web.servlet: debug
org.hibernate.orm.jdbc.bind: trace
org.springframework.cloud.sleuth: info
pattern:
level: '%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]'
spring:
application:
name: member
management:
tracing:
sampling:
probability: 1.0
propagation:
consume: B3
produce: B3
zipkin:
tracing:
endpoint: http://<zipkin 실행중인 ec2 퍼블릭 IPv4 DNS 주소>:9411/api/v2/spans
아래의 블로그에서 yml 작성하는 방식에 대해서 도움받았습니다!
6. 배포 및 테스트
6-1. 에러 발생
- 배포 후에 SNS 메시지를 발행했는데 아래와 같은 에러가 발생했다. 에러가 너무 길어서 내가 Caused by 부분만 정리해서 모았다.
2023-11-12T16:51:31.271+09:00 ERROR [member,,] 2742 --- [ntContainer#0-1] .s.AbstractMessageProcessingPipelineSink : Error processing message 2b7de981-6d33-40a7-b5ea-65c150b8a6b2.
Caused by: io.awspring.cloud.sqs.listener.AsyncAdapterBlockingExecutionFailedException: Error executing action in BlockingMessageListenerAdapter
Caused by: io.awspring.cloud.sqs.listener.ListenerExecutionFailedException: Listener failed to process messages 2b7de981-6d33-40a7-b5ea-65c150b8a6b2
Caused by: java.lang.NullPointerException: Cannot invoke "com.fasterxml.jackson.databind.JsonNode.asText()" because the return value of "com.fasterxml.jackson.databind.JsonNode.get(String)" is null
6-2. 에러의 원인 파악하기
- Sqs리스너 코드에서 발생한 오류였는데 이것은 messageId를 추출할 때 messageNode.get(”messageId”) 값이 없어서 발생한 오류였다.
@Slf4j
@RequiredArgsConstructor
@Service
public class SqsListenerService {
private final ObjectMapper objectMapper;
@SqsListener(value = "${spring.cloud.aws.sqs.nickname-sqs-name}")
public void receiveMessage(String messageJson) throws JsonProcessingException {
JsonNode messageNode = objectMapper.readTree(messageJson);
String topicArn = messageNode.get("TopicArn").asText();
String messageContent = messageNode.get("Message").asText();
// messageId 추출 및 로깅 (만약 메시지에 messageId 정보가 있다면)
String messageId = messageNode.get("messageId").asText();
log.info("Received message from SQS with messageId: {}", messageId);
// Assuming the "Message" is also a JSON string, we parse it to print as JSON object
JsonNode message = objectMapper.readTree(messageContent);
log.info("Topic ARN: {}", topicArn);
log.info("Message: {}", message.toString());
}
}
6-3. 문제점이 발생한 코드 디버깅 하기
- 메시지를 받을 때를 어떤 값을 가지고 있는지 확인하려고 디버깅을 해봤다. 실제 값을 확인해 보니 key가 MessageId로 들어오는데 우리는 messageId를 key로 사용해서 값을 가져오다 보니 null을 리턴해서 생긴 오류였다.
6-4. 코드 수정 후 zipkin 추적 확인하기
- 아래와 같이 추적이 잘 되고 있는 것을 확인할 수 있었다.
7. 마무리
이번에는 ECS에 Zipkin서버를 올리고 SpringBoot와 연동하는 것까지 해봤다. 여기서 지금 추적까지는 성공했지만 다른 많은 문제점들이 발견되었다. 문제점들은 다음과 같다.
1. 서버 간의 추적이 제대로 되고 있지 않다. (나는 프로젝트에서 SNS, SQS를 사용하여 메시지 드리븐으로 데이터 정합성을 보장하는데 Zipkin은 Http요청을 기준으로 추적을 한다. SNS 발행까지는 HTTP 요청이지만 이후의 동작은 HTTP 요청이 아니기 때문에 추적하기가 쉽지 않다.)
2. 테스트 핑을 통해서 들어오는 요청도 http 요청이다 보니 zipkin안에 ALB테스트로 ping을 던지는 내용들이 계속해서 쌓였다. 이게 지속되면 용량에 대한 문제가 발생할 수도 있기에 추후 필요한 메서드만 추적하도록 코드의 수정이 필요할 것 같다.
위의 이슈들로 인해 아직 스프링 부트 서버끼리 제대로 추적되는 모습을 Zipkin으로 확인하지 못한 상황이다. 다음 포스트에서는 이번에 완성하지 못한 MSA에서 2개의 스프링 부트 서버끼리 한 요청에 이어지는 모든 관련된 요청들이 같은 traceId를 가지게 하여 이것을 ZIpkin을 통해서 추적이 가능하도록 구성한 것을 설명하도록 하겠다.
아래의 AWS와 SpringBoot로 MSA의 message-driven 아키텍처를 구성한 글을 본다면 조금 더 이 글이 이해가 될 것이다.
이 포스트는 Team chillwave에서 사이드 프로젝트를 하던 중 적용했던 부분을 다시 공부하면서 기록한 것입니다.
시간이 괜찮으시다면 항상 같이 작업하는 팀원 평양냉면7님의 글도 봐주시면 감사하겠습니다!
'Spring MSA' 카테고리의 다른 글
SpringBoot MSA 로깅: Zipkin으로 서버간 SNS, SQS, Feign 통신의 분산 로그 추적 하기 (1) | 2023.11.20 |
---|---|
[Spring] Util 클래스 - static vs Bean (3) | 2023.11.18 |
[Spring MSA] 스프링 이벤트와 SNS/SQS로 DB 정합성 보장 2탄 - ZeroPayload로 FeignClient 요청 (0) | 2023.11.17 |
[Spring MSA] Spring Event, SNS, SQS를 사용하여 DB 정합성 보장하기 1탄 (2) | 2023.11.15 |
[Spring] SpringFramework 4.2 이후 스프링 이벤트의 변화 (2) | 2023.11.13 |