- 나니트(Nanit)는 아기 수면 상태 분석용 비디오 처리 파이프라인에서 AWS S3를 사용했으나, 초당 수천 건의 업로드로 인해 PutObject 요청 비용이 전체 비용의 대부분을 차지함
- 또한 S3 Lifecycle 규칙의 최소 1일 보존 제한으로, 실제로는 2초 내 처리되는 영상에 대해 24시간 저장 요금을 지불해야 했음
- 이를 해결하기 위해 Rust 기반 인메모리 스토리지 시스템 N3를 구축, S3는 오버플로 버퍼로만 사용
- N3는 SQS FIFO를 통해 기존 처리 파이프라인과 완전히 호환되며, 엄격한 순서 보장과 신뢰성을 유지
- 결과적으로 연간 약 50만 달러의 비용 절감과 함께, 단순하면서도 안정적인 구조를 확보함
배경 및 기존 구조
- 기존 파이프라인은 카메라가 영상을 S3에 직접 업로드하고, Lambda → SQS FIFO → 처리 파드로 이어지는 구조
- 카메라 서비스가 S3 presigned URL을 발급
- Lambda가 업로드된 객체 키를 SQS FIFO 큐에 게시
- 처리 파드가 S3에서 영상을 내려받아 수면 상태 추론 수행
- 이 구조의 장점은 업로드와 처리의 완전한 분리, 자동 확장성, S3의 내구성 및 가용성 보장, SQS를 통한 순서 유지
-
Lifecycle 규칙으로 하루 후 자동 삭제되어, 별도의 GC 관리가 불필요했음
변경 이유
-
PutObject 요청 요금이 가장 큰 비용 요인으로 확인됨
- 초당 수천 건 업로드 시, 객체당 요청 요금이 누적되어 급격히 증가
- 영상 조각(chunk)을 더 작게 나누면 지연은 줄지만 비용은 선형 증가
-
저장 비용도 문제였음
- 처리 완료까지 2초 미만이지만, Lifecycle 최소 1일로 인해 24시간 요금 부과
- 목표는 순서 보장과 신뢰성 유지하면서, 객체당 요금 없는 경로(happy path) 를 확보하는 것
설계 원칙 및 요구사항
-
단순성: 복잡한 구현 대신 아키텍처 수준에서 단순화
-
정확성: 기존 파이프라인에 완전 호환되는 drop-in 대체재
-
효율성: 정상 경로는 N3에서 처리, 예외 상황만 S3로 폴백
- 설계 제약
- 객체는 수 초 단위 생존, 엄격한 순서 보장, 초당 수천 업로드
-
카메라 펌웨어 변경 불가, 소규모 손실 허용, 대규모 백로그 허용
-
비용 최소화: S3 요청 및 대기 저장 비용 제거
N3 아키텍처 개요
-
N3는 메모리 기반 임시 저장소로, 영상이 처리되기 전까지 약 2초간만 보관
-
S3는 N3가 과부하일 때만 사용
- 구성 요소
-
N3-Proxy (무상태) : 외부 업로드 수신 및 내부 URL 발급
-
N3-Storage (상태 보유) : RAM에 영상 저장 후 SQS에 다운로드 URL 게시
- 처리 파드는 SQS에서 URL을 받아 N3 또는 S3에서 다운로드
정상 경로 (Happy Path)
- 카메라가 업로드 URL 요청
- 카메라 서비스가 N3-Proxy 내부 API 호출
- 카메라가 N3-Proxy 외부 엔드포인트로 업로드
- N3-Proxy가 N3-Storage로 전달
- N3-Storage가 영상을 메모리에 저장하고 SQS에 URL 게시
- 처리 파드가 N3에서 다운로드 후 처리
2단계 폴백 구조
-
1단계(요청 단위) : N3-Storage가 업로드를 수용하지 못하면 N3-Proxy가 대신 S3로 업로드
-
2단계(클러스터 단위) : N3 전체가 비정상일 경우, 카메라 서비스가 직접 S3 URL을 발급
구성 분리 이유
-
장애 격리: Storage 장애 시 Proxy는 S3로 우회 가능
-
리소스 분리: Proxy는 CPU/네트워크 중심, Storage는 메모리 중심
-
보안성: Storage는 외부 네트워크 미노출
-
배포 안정성: Proxy는 무상태로, 업데이트 시 데이터 손실 없음
설계 검증 및 PoC
- 주요 불확실성: 용량 산정, RAM만으로 충분한지, 노드 장애 처리, GC 정책, 재시도 동작
- 두 가지 접근 병행
-
Synthetic Stress Test: 부하·지연·다운타임 시 한계점 측정
-
Production PoC (Mirror Mode) : 실제 트래픽 복제, S3와 N3 병행 기록
- 특정 카메라 그룹만 테스트, Unleash로 실시간 전환
-
지표 대시보드로 N3/S3 경로 비교
주요 발견
-
TLS 종료가 CPU의 주요 병목, AWS 버스트 네트워크 제한 존재
-
RAM 기반 저장소 충분, 디스크 불필요
-
Delete-on-GET 안전, 재다운로드 없음
-
TTL 기반 GC 필요, 처리 누락 세그먼트 정리
구현 세부 사항
DNS 기반 로드밸런싱
-
n3-proxy는 DaemonSet으로 노드당 1개 배치
-
Route53 Multi-Value A 레코드로 저비용 DNS 부하분산 구성
- 각 노드별 IP 1개, 30초 TTL, 헬스체크 기반 자동 제거
- 롤아웃 시 SIGTERM → Not Ready → 연결 드레인 → 재시작 절차로 무중단 배포
네트워크 성능 문제
- 초기 벤치마크에서 1분 후 RPS가 1k → 70으로 급감
- 원인: AWS의 버스트형 네트워크 크레딧 소진
- 해결: c8gn.4xlarge (50Gbps) 네트워크 최적화 인스턴스로 전환
HTTPS 최적화
- 초기 stunnel 사용 시 CPU 병목 발생
- 개선 사항
-
rustls로 전환
-
Graviton4 인스턴스 업그레이드
-
Rust 컴파일 최적화 플래그 적용
- 결과: 동일 비용 대비 RPS 30% 향상
아웃바운드 트래픽 분석
- 업로드만 수행하므로 egress가 적을 것으로 예상했으나, TLS 핸드셰이크 및 ACK로 트래픽 발생
- TLS 인증서 전송 약 7KB
-
tcpdump 결과, 출력 바이트의 84%가 ACK 프레임
-
TCP 타임스탬프 비활성화로 ACK 크기 12바이트 절감
- 단, 시퀀스 번호 래핑 위험 존재 → 업로드 단위 소켓 재생성, 1GB 전송 후 소켓 재활용으로 완화
메모리 누수 문제
-
n3-proxy 메모리 지속 증가, jemalloc 분석 결과 hyper BytesMut 버퍼 누적
- 원인: 중단된 업로드 연결 미정리
- 해결
-
10분 이상 유휴 연결 종료
-
keep-alive 비활성화, 타임아웃 강화
- 업로드 완료 후 즉시 소켓 종료
스토리지 구조
- 단순한 인메모리 DashMap 사용
- 업로드 시 bytes_used 증가, 다운로드 시 감소
- 80% 용량 초과 시 업로드 거부 및 Proxy에 신호 전달
-
Control 핸들로 업로드·GC 일시 중지 가능
그레이스풀 재시작
- 메모리 기반이라 재시작 시 데이터 손실 방지 필요
-
SIGTERM 수신 → Not Ready → 기존 다운로드만 처리 → 요청 종료 후 재시작
- 일반적으로 수 초 내 드레인 완료
GC 메커니즘
-
다운로드 시 즉시 삭제(Delete-on-GET)
-
TTL 기반 GC로 미처리 세그먼트 정리
-
유지보수 모드에서는 GC 일시 중단 가능
결론 및 교훈
-
N3를 기본 경로, S3를 폴백 버퍼로 사용해 연간 약 50만 달러 절감
-
핵심 통찰: 단명 객체(2초 내 처리)에 대해 완전한 내구성은 불필요, 인메모리 저장소로 충분
-
S3는 장기 보관·장애 시 신뢰성 보장, 두 시스템의 장점을 결합
-
명확한 제약 정의와 Mirror PoC 검증이 과도한 설계 방지에 핵심 역할
-
직접 구축이 유효한 경우
- 충분한 트래픽 규모로 비용 절감 효과가 명확할 때
-
단순한 요구 조건으로 유지보수 부담이 낮을 때
- 나니트는 현재도 이 시스템을 안정적으로 운영 중이며, 복잡성 없이 신뢰성을 확보한 사례로 평가됨