ecsimsw
이벤트 전달 유실 개선, SNS+SQS를 선택한 이유 본문
이벤트 전달 구조 개선
회사의 기기 이벤트 전달 구조를 SNS+SQS 조합으로 개선한 경험을 소개한다. 기존에는 아래 그림에서 Event producer가 Http로 각 서비스에 이벤트를 전달했는데, 기기가 늘어남에 따라 많아진 이벤트를 빠르게 처리할 수 있으면서도 유실에 안정적인, 그러면서도 서비스 확장에 유연한 아키텍처를 고민하게 되었다. 작은 팀이니만큼 러닝 커브가 적고, 인프라 비용에 큰 부담이 없었으면 좋겠다는 생각이었다.
이벤트 전달 구조 / SQS 도입 이유
1. 직접 전달, 이벤트 유실과 의존 문제
Http 직접 전달은 위험하다. 수신하는 서버에 문제가 발생하는 경우, 그 시간 동안 직접 전달된 이벤트는 유실된다. 또 전송자는 수신하는 서버의 정보를 모두 알아야 하기 때문에, 수신 서비스가 늘어나는 경우처럼 서비스 확장에 유연하지 못하다.
2. Kafka와 MQ, 인프라 운영 비용
Kafka를 사용하여 이벤트를 N 시간 동안 보관, 여러 Service에서 서로 다른 Consumer group으로 이벤트를 처리하는 꼴을 생각할 수 있다. 또는 Service 별로 Message Queue를 두고 처리 후 메시지를 제거하는 구조를 생각할 수 있다. 이 두 구조 모두 Kafka, Message Queue의 고가용성이 매우 중요할 것이다. AWS Full managed를 고민하였고, AWS MSK와 AWS SQS의 비용을 비교하였다.
AWS MSK의 경우 고가용성을 유지하기 위해 3개의 AZ 클러스터를 필요로 한다. 2 cpu, 8 GiB의 m5.large를 사용한다고 하면, 시간당 0.258 USD로 월에 557 USD가 필요하다. 이때 이벤트를 보관하는 스토리지 비용과 데이터 전송을 위한 네트워크 비용이 추가된다. 반면 SQS는 사용하는 만큼 비용이 발생한다. 요청 1백만 개당 0.4 USD로, 월 1억 개의 요청에 40 USD이다.
요청 수가 많지 않다면 관리 포인트가 없고 저렴한 SQS는 좋은 선택지가 될 것이다. 특히 우리 팀은 주요 서비스들부터 빠르게 적용할 수 있는 구조를 우선 도입하고, 이후 점진적인 개선을 생각하고 있기에, 초반부터 큰 요청 수가 아닌 관리가 쉽고 러닝 커브가 낮으면서도, 가격이 저렴한 SQS가 더 적합했다.
SNS를 함께 사용하는 이유
SNS의 Topic 별로 수신자를 지정할 수 있다. 수신 타입은 SQS가 될 수 도 있고, Lambda나 Http 요청으로 이어질 수 도 있다. SNS와 SQS를 함께 사용한 조합을 팀에서 선정한 이유 중 가장 컸던 두 가지 근거를 소개한다.
1. 유연한 확장
만일 이벤트를 수신하는 수신자를 늘린다고 가정해 보자. SQS를 직접 사용하고 있을 경우에는 Event producer가 추가된 SQS의 정보를 알고 직접 전달해야 할 것이다. 이런 Event producer가 다음 서비스를 직접 의존하고 있는 꼴은 서비스 확장을 고려하고 있는 팀에 좋지 않은 구조라고 생각했다.
그보다는, Event producer는 SNS의 정보만을 알고, 전달해야 하는 수신자에 대한 정보나 의존 없이 필요한 토픽만을 정의하고 SNS에 전달하는 꼴이 더 확장에 유연했다. 수신자가 추가될 때는 Event producer의 변경 없이 수신자가 SNS에서 구독하고자 하는 토픽만 결정하면 될 뿐이다.
2. Fan out 패턴
Fan out 패턴이란 한 메시지를 여러 수신자에서 처리하는 구조를 말한다. 이를 테면 여러 서비스에서 처리되어야 하는 이벤트 타입이 존재하는 경우, SQS를 직접 사용하면 Producer는 각 필요한 SQS에 직접 전달해야 하지만, SNS를 사용하는 경우에는 하나의 토픽으로 이벤트를 전달하면 필요한 여러 서비스에서 전달받아 각자의 처리를 수행할 수 있게 된다.
SQS 동작 원리
1. FIFO vs Standard
SQS는 FIFO와 Standard 타입이 존재한다. FIFO 타입은 큐에 전달된 메시지의 순서를 유지하여 처리한다. 큐에서 메시지 순서 유지가 필요한 메시지들끼리 동일한 Group id로 묶어 순서를 유지할 수 있다. 순서가 유지되기 때문에 같은 Group 안에서는 병렬 처리가 불가능하며 가장 우선순위의 메시지 처리에 지연이 발생하면 뒤따르는 모든 메시지 처리에 지연이 된다. 메시지를 해싱하여 Duplication id를 갖고 있기 때문에 Producer의 메시지 중복 발행에 안전하다. 기본 5분간 중복 발행을 막는다.
Standard 타입은 순서를 보장하지 않는다. 그래서 병렬 처리가 가능하고 FIFO보다 처리량이 높다. 단, 이벤트 처리 순서가 반드시 보장되어야 하는 경우나 FIFO의 Duplication id처럼 꼼꼼한 Producer의 메시지 중복 발행이 보장되어야 하는 경우 적절하지 않다.
2. 가시성 타임 아웃
SQS는 메시지의 처리 결과를 수신하여 큐에서 메시지를 제거한다. 반대로 수신되어 처리되는 와중에는 메시지는 큐에 그대로 쌓여있다. 이를 다른 수신자들이 처리하려고 하면 중복이 발생할 테니 SQS는 컨슈머에 수신된 메시지를 일정 시간 동안 노출하지 않는 시간제한을 둔다. 이를 가시성 타임 아웃이라고 한다.
만약 가시성 타임 아웃이 너무 짧으면 메시지가 처리되는 와중에 타임 아웃이 끝나 다른 컨슈머에게 메시지가 노출되고, 이는 한 메시지를 중복 처리되는 상황을 만들 수 있다. 그렇다고 가시성 타임 아웃 시간이 너무 길게 되면 처리에 실패했을 때 타임 아웃 시간까지 다른 수신자에게 노출되지 못하여 처리가 너무 늦어지는 문제가 발생하게 된다.
운영 방법
1. 순서 보장과 처리량
기기의 이벤트 순서가 보장되어야 한다. 단 각 디바이스에서 나온 이벤트끼리의 순서만 보장되면 되고, 모든 디바이스는 고유의 id를 갖는다. 이를 테면 전구의 On, Off 이벤트가 빠르게 일어났고, 그 순서가 바뀌어 처리된다면 UI와 사용자에 혼돈이 발생할 것이다. SQS의 타입을 FIFO로 하고, 기기 이벤트의 그룹 id를 device id 값으로 하여 기기별 순서를 보장할 수 있었다.
FIFO로 했을 때 HOL blocking 문제를 조심해야 한다. 그룹 별로, 즉 디바이스 별로 순서 보장이 되는 상황에서 가장 앞에 있는 이벤트 처리가 지연되는 경우, 또는 재시도 처리가 이어지는 경우, 해당 이벤트가 버려질 때까지 그 디바이스의 이벤트를 처리할 수 없다. 재시도 처리를 적은 횟수, 짧은 간격으로 처리하는 것이 좋겠다.
2. 재시도 처리와 DLQ
SQS에서 메시지를 수신하여 정상 처리를 알려야만 큐에서 메시지가 제거된다. 그리고 가시성 타임 아웃이 끝나면 다시 메시지가 수신 가능 상태가 되는데, 이를 반대로 말하면 'SQS의 메시지는 정상 처리되기까지 가시성 타임 아웃 시간마다 재시도 처리된다'와 같다. 이 재시도는 큐에 메시지 보관 시간까지 무한히 반복된다.
만약 처리할 수 없는 메시지가 있다면 N번 이상의 재시도 반복은 의미가 없을지도 모른다. DLQ를 설정하여 N번의 재시도 후에도 처리할 수 없는 메시지를 따로 저장할 수 있도록 설정하였다. DLQ의 처리되지 못한 메시지를 처리할 수 있는 Service를 정의하거나, DLQ의 메시지를 Queue로 재등록하는 Redrive를 사용하여 처리에 실패한 메시지를 재시도한다.
우리 팀은 인지하고 있는 예외 상황과 인지하지 못하는 예외 상황을 나눴다. 인지하고 있는 예외, 예를 들어 이벤트 포맷 자체에 문제가 있어 DTO 컨버팅에 실패하는 경우에 이는 재시도 처리할 필요가 없다. 이런 예외의 이벤트는 직접 DLQ에 전달한다. 반대로 인지하지 못하는 예외의 경우에는 애플리케이션 내부에서 빠른 재시도 처리된다. 기기 이벤트 특성상 꼼꼼한 재시도 처리보다 빠른 컨슘과 다음 이벤트 처리가 더 중요하다고 판단해서이다.
3. 중복 수신 대비
메시지 처리가 가시성 타임 아웃보다 늦어지면, 두 개 이상의 컨슈머에서 같은 메시지를 처리하는 경우가 발생하게 된다. 가장 쉽게 생각할 수 있는 방법은 DB 트랜잭션을 활용하는 방법이었다. 트랜잭션 안에서 메시지를 해시한 값을 유니크 컬럼으로 기록하여 동일한 메시지가 중복해서 처리되는 경우를 막을 수 있다.
우리 팀처럼 이벤트 처리 시간이 매우 짧고, 처리해야 하는 양이 많은 상황에서, 아주 드물게 발생하는 특별한 예외 상황을 대비하여 모든 메시지에 해시 처리, 멱등성 처리를 하는 것이 과하다고 생각했다. 모기를 잡는데 칼을 뽑는 격이다. 그보다는 예외가 발생하면 빠르게 DLQ로 넘기고 해당 메시지를 큐에서 제거하여 다른 컨슈머가 동시에 같은 메시지를 수신하는 경우를 피하는 것으로 더 빠르게 예외를 처리하는 꼴이 더 팀에 적합하다고 생각했다.
정리하자면, SQS의 중복 수신 문제는 가시성 타임 아웃 시간까지 메시지를 못 처리했을 때, 다른 수신자가 처리 중인 메시지를 컨슘해서 중복 처리하는 경우에 발생하기에, 재시도를 최소한으로 하고, 처리 시간을 짧게 하여 가시성 타임 아웃보다 늦어지는 상황이 없도록 조정하였다.
'KimJinHwan > Project' 카테고리의 다른 글
DB 커넥션 부족을 잡았던 경험 (2) | 2025.03.03 |
---|---|
팀에서 테라폼을 도입하고 얻은 것들 (0) | 2025.01.01 |
대기열 사이즈와 OOM 문제 해결 (0) | 2024.12.25 |
S3 업로드 속도 개선, Pre-signed url과 Thumbnail Lambda (0) | 2024.05.31 |
현재 사용 불가능한 API의 응답을 자동 생성해주는 라이브러리 (0) | 2024.01.17 |