일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 클린코드
- 객체지향설계
- 플레이 스토어 20명
- git
- 구글 플레이 스토어 배포 방법
- 기능명세서
- 플레이스토어 비공개 테스트
- 운영체제 #CS지식
- 객체지향
- 설계
- 구글 비공개 테스트 20명
- 구글 플레이 비공개 테스트
- 우테코
- 프리코스
- 커밋 메시지
GYUD-TECH
[뉴젯] 메일 수신 처리 속도 및 AWS Throttling 지표 개선 본문
뉴젯 서비스는 하루 평균 10,000개 정도의 뉴스레터를 수신하고 있습니다. 메일들은 Node.js의 SimpleParser 라이브러리를 통해 파싱 이후 메타데이터와 함께 DB 및 파일 저장소에 저장됩니다.
이번 글에서는 현재 아키텍처에서 발생한 문제들을 소개하고, 실험을 통해 해결책을 찾아가는 과정에 대해 자세히 소개합니다.
문제 상황
서버 비용 절약을 위해 파싱 작업 역시 Supabase API 서버에서 처리하였기 때문에, 평균 3초 정도의 긴 시간이 소모되었습니다. 또한 Supabase API 서버에 메일 파싱 요청을 전달하는 Lambda 함수도 메일 저장 여부 확인 후 종료되는 방식으로 구현되어 있어 함수의 Duration, Throttling 지표가 높게 측정되었습니다. 이로 인해 Lambda의 리소스를 사용하지 않음에도 실행시간이 길어 비용이 높게 측정되는 문제가 발생하였습니다.
문제 해결
1. 메일 파싱 역할을 Lambda 함수로 이전
가장 먼저 메일 수신 및 데이터 파싱 작업을 Lambda의 역할로 분리하여 API 서버의 부담을 줄이고 파싱 성능을 개선하고자 하였습니다. 이를 위해 Lambda의 함수에서 파싱 기능을 수행하는 parser.mjs 파일을 만들어 파싱 작업을 수행하도록 하였습니다.
import { simpleParser } from "mailparser";
export async function parseEmail(emailContent) {
try {
const parsed = await simpleParser(emailContent);
const fromName = parsed.from?.value[0].name;
const fromDomain = parsed.from?.value[0].address;
const toDomain = parsed.to?.value[0].address;
const maillingList = parsed.headers.get("list")?.id?.name ?? null;
const htmlContent = parsed.html || "No HTML content";
return { fromName, fromDomain, toDomain, maillingList, htmlContent };
} catch (error) {
throw new Error(`Failed to parse email: ${error.message}`);
}
}
또한, 동일한 시간에 많은 이메일이 수신되는 뉴스레터의 특성을 고려하여 Lambda 함수의 동시성을 최대 10으로 설정해 동시에 여러대의 Lambda에서 메일 파싱 작업을 수행하도록 설정하였습니다.
이를 통해 기존 방식처럼 람다 실행 시간 동안 Supabase 서버의 응답을 기다리기만 하는 것이 아니라 직접 리소스를 사용해 동일 실행 시간 대비 효율성을 개선하였습니다. 또한 메일 원본 요청, 파싱 작업 수행, 메일 html 저장소에 파일 업로드 작업을 API 서버의 역할에서 없에고, 저장된 메일 html의 objectKey만을 수신받도록 하여 불필요한 네트워크 요청을 줄일 수 있었습니다.
2. 배치 처리 및 Throttling 지표 개선를 위한 Message Queue 도입
1의 방식을 도입하였음에도 Lambda의 Throttling 지표는 개선되지 않았습니다. 뉴스레터의 특성상 짧은 시간에 많은 메일이 전송되기 때문에 수신한 메시지를 저장해 두었다, 실행하는 로직이 필요하였습니다. 실제로 뉴젯에서 가장 인기있는 뉴스레터 뉴닉은 594명이 구독중입니다. 뉴닉 뉴스레터가 발송되면 10초 이내에 총 594개의 메일이 도착하기 때문에 Throttling 지표가 여전히 높게 측정되었습니다.
이를 해결하기 위하여 Lambda 함수 호출 이전에 Message Queue를 두어 Throttling 지표를 개선하고, 배치 처리를 수행하도록 하였습니다. 여러 메시지 큐 서비스 중, Lambda와의 연동성과 빠른 구현 속도, 프리티어로 인한 비용 절약성을 고려하여 AWS SQS를 사용하여 메시지 큐를 구현하였습니다.
뉴젯에서 평균 뉴스레터 구독자 수는 50명 ~ 70명인 것을 고려하여 배치 기간은 5로 설정하였습니다. 또한 적당한 배치 대기 시간을 찾기 위해 3초 ~ 8초까지 실험을 진행한 결과 5초로 설정하였을 때, 70% 이상이 배치 크기를 모두 채우는 것을 확인하여 배치 크기는 5로 설정하였습니다.
트리거 생성 이후에는 Lambda 함수를 수정하여 배치 처리가 가능하도록 구현하였고, 테스트를 위해 random 함수를 활용해 메일 수신 실패 실험을 진행하였습니다. 하지만 배치 실행된 이벤트 중 하나라도 실패할 경우 5개 모두 SQS 재시도 로직에 추가되는 문제가 발생하였습니다.
AWS 공식 문서를 통해 SQS 트리거의 Lambda 함수 실패 보고 형식을 파악하였고, 해당 형식에 맞추어 다시 코드를 작성하고, 트리거 응답 타입을 ReportBatchItemFailures로 설정하여 문제를 해결하였습니다.
export const handler = async (event) => {
const batchItemFailures = [];
for (const record of event.Records) {
try {
if(Math.random() < 0.5) {
throw new Error("Random error occurred");
}
// 메일 처리
} catch (error) {
logError("Failed to process message", { error: error.message });
batchItemFailures.push({ itemIdentifier: record.messageId });
}
}
if (batchItemFailures.length > 0) {
logError(`Failed to process ${batchItemFailures.length} events`);
} else {
logInfo("Batch processing completed successfully");
}
return { batchItemFailures };
};
aws lambda update-event-source-mapping —uuid [트리거 UUID] /
—function-response-types ReportBatchItemFailures
이를 통해 실패한 이벤트만 SQS에 반환하여 재실행 하도록 하여 불필요한 재시도 이벤트를 제거할 수 있었습니다.
결과적으로 배치 처리 및 SQS의 도입으로 50개 동시 전송 메일에 대해 Throttling 지표가 0으로 측정되는 것을 확인하였습니다. 또한 메일 호출 횟수가 1/4 수준으로 줄어, Lambda 로드 시간이 절약되며 개당 평균 메일 파싱 시간이 3초에서 1.2초로 단축되었습니다.
3. 메일 수신 실패 처리를 위한 DLQ 도입
메일 수신 실패 시 재시도 로직은 해당 이벤트가 다시 SQS에 추가되고, 1분 후 재시도를 수행하는 방식으로 동작합니다. 최대 재시도 횟수는 3회로 설정하여 무한 재시도 실행으로 인한 리소스 낭비를 방지하고자 하였습니다. 하지만 메일 수신 실패 실험에서, 최대 재시도 횟수를 초과한 메일은 따로 보관되지 않고 삭제되는 문제를 확인하였습니다.
현재의 방식으로는 오류가 발생하더라도, 로그를 일일이 확인하지 않는다면, 오류 발생 여부를 판단하기 어렵다는 문제가 있었습니다. 따라서 별도의 메시지 큐를 두어 수신에 실패한 메시지는 별도 보관하도록 설정하였습니다.
구현 이후, 50% 확률로 에러를 발생 시키도록 실험을 설계하고 총 51개의 S3 데이터 삽입 이벤트를 발생시켜 재시도 로직 및 DLQ 메시지 저장 여부를 확인하였습니다. 실험 결과, 아래 사진과 같이 3번의 시도에서 각각 51, 31, 12번의 이벤트가 실행되었고, 람다 함수에서 재시도된 것을 확인하였습니다.
마지막 12번의 이벤트 중에서는 7개의 이벤트에서 에러가 발생하며 DLQ에 총 7개의 이벤트가 저장된 것도 확인할 수 있었습니다.
추가 개선 사항
현재 기존 뉴젯의 Supabase Deno 서버 코드를 Spring 서버로 이전하는 작업을 진행하고 있습니다. 기존 API코드는 빠른 MVP 개발을 목표로 작성되었기 때문에 개발 시간 단축을 위해 테스트 코드가 부족하고, 도메인이 아닌 Service layer에 비즈니스 로직이 작성되어 있습니다. 현재까지도 일정한 사용자가 유지되고 있기 때문에 이를 Spring 서버로 이전하여 서비스의 확장성을 높이고 기술적 역량을 기르고자 이전 작업을 진행하고 있습니다.
이전 작업이 완료되면 API 서버에서도 배치 처리와 캐싱을 적용하여 데이터 조회 및 쓰기 성능을 개선할 계획입니다.
참고 자료
'프로젝트' 카테고리의 다른 글
[뉴젯] Redis 도입과 분산 락을 활용한 동시성 문제 해결 (0) | 2025.02.01 |
---|---|
[뉴젯] 도커를 활용한 CI 구축 (0) | 2025.01.15 |
[뉴젯] 유저 지표에서 시작된 쿼리 성능 최적화 (1) | 2025.01.06 |
[해피에이징] 수평적 권한 상승 문제 (0) | 2024.01.11 |
[해피에이징] User 데이터 삭제 (0) | 2024.01.08 |