# 자체 “S3”를 구축해 연간 50만 달러를 절감한 방법

> Clean Markdown view of GeekNews topic #23951. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=23951](https://news.hada.io/topic?id=23951)
- GeekNews Markdown: [https://news.hada.io/topic/23951.md](https://news.hada.io/topic/23951.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2025-10-27T23:34:50+09:00
- Updated: 2025-10-27T23:34:50+09:00
- Original source: [engineering.nanit.com](https://engineering.nanit.com/how-we-saved-500-000-per-year-by-rolling-our-own-s3-6caec1ee1143)
- Points: 21
- Comments: 3

## Summary

Nanit는 초당 수천 건의 비디오 업로드로 인해 **S3 PutObject 요청 비용**이 폭증하자, 이를 대체할 **Rust 기반 인메모리 스토리지 시스템 N3**를 직접 구축했습니다. N3는 **SQS FIFO**와 완전히 호환되며, 정상 경로에서는 RAM에 데이터를 잠시 보관하고 필요 시에만 **S3를 오버플로 버퍼로 활용**해 연간 약 **50만 달러의 비용 절감**을 이뤘습니다. 핵심은 “더 똑똑한 코드”가 아니라 **단순한 아키텍처 설계**였고, 짧은 수명 객체라는 특수한 워크로드를 정확히 이해한 덕분에 가능한 선택이었습니다. 대규모 트래픽을 다루는 팀이라면, 관리형 서비스의 편의성 뒤에 숨은 **요청 단위 비용의 함정**을 다시 생각하게 만드는 사례입니다.

## Topic Body

- 나니트(Nanit)는 아기 수면 상태 분석용 **비디오 처리 파이프라인**에서 AWS **S3**를 사용했으나, 초당 수천 건의 업로드로 인해 **PutObject 요청 비용**이 전체 비용의 대부분을 차지함  
- 또한 **S3 Lifecycle 규칙**의 최소 1일 보존 제한으로, 실제로는 2초 내 처리되는 영상에 대해 **24시간 저장 요금**을 지불해야 했음  
- 이를 해결하기 위해 **Rust 기반 인메모리 스토리지 시스템 N3**를 구축, S3는 **오버플로 버퍼**로만 사용  
- N3는 **SQS FIFO**를 통해 기존 처리 파이프라인과 완전히 호환되며, **엄격한 순서 보장**과 **신뢰성**을 유지  
- 결과적으로 연간 약 **50만 달러의 비용 절감**과 함께, 단순하면서도 안정적인 구조를 확보함  
  
---  
  
### 배경  
  
#### 비디오 처리 파이프라인 개요  
- Nanit의 카메라가 비디오 청크를 녹화하고, **Camera Service**로부터 **S3 presigned URL**을 요청하여 **S3에 직접 업로드**하는 구조  
- AWS Lambda가 객체 키를 **SQS FIFO 큐**에 게시하고(baby_uid로 샤딩), 비디오 처리 파드가 SQS에서 소비하여 S3에서 다운로드 후 수면 상태 추론 수행  
- 이 설정의 장점  
  - S3 착지 + SQS 큐잉이 **카메라 업로드와 비디오 처리를 분리**하여 유지보수나 일시적 다운타임 중에도 비디오 손실 방지  
  - S3를 통해 가용성과 내구성을 직접 관리할 필요 없음  
  - **SQS FIFO + 그룹 ID**로 아기별 순서 보존, 처리 노드는 대부분 무상태로 유지 가능  
  - **S3 Lifecycle 규칙**이 가비지 컬렉션을 담당하여 처리된 비디오 추적 불필요  
  
#### 변경이 필요했던 이유  
- **PutObject 비용이 지배적**: 비디오는 **수 초 동안만** 착지하여 처리되는 **짧은 수명 객체**인데, 초당 수천 건의 업로드 규모에서 **객체당 요청 비용**이 가장 큰 비용 동인  
  - **청킹 빈도를 늘려**(더 많은 작은 청크 전송) 지연 시간을 줄이면 각 추가 청크가 또 다른 PutObject 요청이므로 **비용이 선형적으로 증가**  
- **스토리지가 2차 과세**: 처리가 약 2초 만에 완료되어도 **Lifecycle 삭제 규칙**은 약 **24시간의 스토리지 비용 부과**  
- **신뢰성과 엄격한 순서 보장을 유지**하면서 **정상 경로에서 객체당 비용을 회피**하고 "대기 비용 지불" 스토리지를 최소화하는 설계 필요  
  
### 계획  
- ## 설계 원칙  
  - **아키텍처를 통한 단순성**: 영리한 구현이 아니라 **설계 수준에서 복잡성 제거**  
  - **정확성**: 파이프라인의 나머지 부분에 **투명한 완전한 대체품**  
  - **정상 경로에 최적화**: 일반적인 경우를 위해 설계하고 **엣지 케이스에는 S3를 안전망으로 사용**, 처리 알고리듬이 가끔의 간격에 강건하므로 복잡한 보장 구축보다 단순성 우선  
- ## 설계 동인  
  - **짧은 수명 객체**: 세그먼트가 착지 영역에 수 초 동안만 존재  
  - **순서**: 아기별 엄격한 시퀀싱(최신 것을 먼저 처리하지 않음)  
  - **처리량**: 초당 수천 건의 업로드, 세그먼트당 2-6 MB  
  - **클라이언트 제한**: 카메라의 **재시도 횟수 제한**, 재전송 가정 불가  
  - **운영**: 유지보수/스케일업 중 **수백만 항목의 백로그** 허용  
  - **펌웨어 변경 없음**: 기존 카메라와 작동해야 함  
  - **손실 허용성**: 매우 작은 간격은 허용 가능, 알고리듬이 마스킹  
  - **비용**: 정상 경로에서 객체당 S3 비용 회피, "대기 비용 지불" 스토리지 최소화  
  
### 설계 개요 (N3 정상 경로 + S3 오버플로우)  
- ## 아키텍처  
  - N3는 처리가 드레인하는 데 필요한 시간(약 2초) 동안만 **비디오를 메모리에 보관**하는 맞춤형 착지 영역, **N3가 부하를 처리할 수 없을 때만 S3 사용**  
  - **두 개의 컴포넌트**  
    - **N3-Proxy**(무상태, 이중 인터페이스)  
      - 외부(인터넷 연결): presigned URL을 통해 카메라 업로드 수락  
      - 내부(프라이빗): Camera Service에 presigned URL 발급  
    - **N3-Storage**(상태 보존, 내부 전용): 업로드된 세그먼트를 RAM에 저장하고 **파드 주소 지정 가능한 다운로드 URL**로 SQS에 큐잉  
  - 비디오 처리 파드는 SQS FIFO에서 소비하고 URL이 가리키는 스토리지(N3 또는 S3)에서 다운로드  
- ## 정상 흐름 (Happy Path)  
  - 카메라가 Camera Service에서 업로드 URL 요청  
  - Camera Service가 N3-Proxy의 내부 API에서 presigned URL 요청  
  - 카메라가 N3-Proxy의 외부 엔드포인트에 비디오 업로드  
  - N3-Proxy가 N3-Storage로 전달  
  - N3-Storage가 비디오를 메모리에 보관하고 자신을 가리키는 다운로드 URL로 SQS에 큐잉  
  - 처리 파드가 N3-Storage에서 다운로드하여 처리  
- ## 2계층 폴백  
  - **Tier 1: 프록시 수준 폴백**(요청당)  
    - 메모리 압박, 처리 백로그, 파드 장애 등으로 **N3-Storage가 업로드를 받을 수 없으면** N3-Proxy가 카메라를 대신하여 S3에 업로드  
    - 카메라는 장애가 감지되기 전에 N3 URL을 받은 상태  
  - **Tier 2: 클러스터 수준 재라우팅**(모든 트래픽)  
    - N3-Proxy 또는 N3-Storage가 비정상이면 Camera Service가 **N3 URL 발급 중단**하고 S3 presigned URL을 직접 반환  
    - N3가 복구될 때까지 모든 트래픽이 S3로 흐름  
- ## 두 개의 컴포넌트로 분리한 이유  
  - **장애 반경**: 스토리지가 충돌해도 프록시는 S3로 라우팅 가능, 프록시 충돌 시 해당 노드의 트래픽만 영향받고 전체 스토리지 클러스터는 무사  
  - **리소스 프로파일**: 프록시는 CPU/네트워크 집약적(TLS 종료), 스토리지는 메모리 집약적(비디오 보유), **다른 인스턴스 타입과 스케일링 요구사항**  
  - **보안**: 스토리지는 인터넷을 절대 접촉하지 않음  
  - **롤아웃 안전성**: 프록시(무상태) 업데이트 시 스토리지(활성 데이터 보유) 건드리지 않음  
  
### 설계 검증  
- ## 검증 필요사항  
  - **용량 및 크기 조정**: 클라이언트 네트워크 전반의 실제 **업로드 지속 시간**, 필요한 컴퓨팅 및 **업로드 버퍼 크기**  
  - **스토리지 모델**: 모든 것을 **RAM**에 보관 가능한지 또는 **디스크** 필요 여부  
  - **복원력**: 저렴하게 **로드 밸런싱**하고 **장애 노드**를 처리하는 방법  
  - **운영 정책**: **GC** 필요사항, **재시도** 기대치, **GET 시 삭제**가 충분한지 여부  
  - **알려지지 않은 미지수**: 아이디어가 현실과 만날 때 어떤 엣지 케이스가 나타날지  
- ## 접근법 1: 합성 스트레스 테스트  
  - 다양한 동시성, 느린 클라이언트, 지속적인 부하, 처리 다운타임으로 시스템을 **한계까지 밀어붙이는 로드 생성기** 구축  
  - **목표**: 한계점 발견, 예상하지 못한 병목 현상 파악, 용량 계획을 위한 **결정론적 기준선** 확보  
- ## 접근법 2: 프로덕션 PoC (미러 모드)  
  - 합성 테스트로는 **실제 카메라 동작 복제 불가**: 불안정한 Wi-Fi, 다양한 펌웨어 버전, 예측 불가능한 네트워크 조건  
  - **미러 모드**: n3-proxy가 먼저 **S3에 작성**(프로덕션 보존) 후 PoC **N3-Storage**(카나리 SQS + 비디오 프로세서에 연결)에도 작성  
  - **대상 코호트**: 펌웨어 버전 / Baby-UID 목록별로  
  - **데이터 패리티**: PoC와 프로덕션의 **수면 상태** 비교, 차이 조사  
  - **관찰 가능성**: 경로별 대시보드(N3 vs S3), 큐 깊이, 지연 시간/RPS, 오류 예산, 이그레스 분석  
  - **기능 플래그**(Unleash 사용)가 중요: 배포 없이 **실시간으로 코호트 전환** 가능, 좁은 슬라이스(오래된 펌웨어, 약한 Wi-Fi 카메라) 테스트 후 문제 발생 시 즉시 복원  
- ## 발견한 내용  
  - **병목 현상**: TLS 종료가 대부분의 CPU 소비, **AWS 버스트 가능 네트워킹**이 크레딧 소진 후 스로틀링  
  - **메모리 전용 스토리지가 실행 가능**: 실제 업로드 시간 분포와 동시성을 통해 **작업 세트를 RAM에 안전한 여유를 두고 저장** 가능 확인, 디스크 불필요  
  - **TCP 타임스탬프 오버헤드**: 전송된 총 바이트 중 약 **85%가 ACK 프레임**, TCP 타임스탬프 비활성화(`sysctl -w net.ipv4.tcp_timestamps=0`)로 **ACK당 12바이트 절감**  
    - 위험: 동일 소켓에서 높은 바이트 수 전송 시 시퀀스 번호 래핑, 지연된 패킷 오병합으로 손상 가능  
    - 완화: (1) 업로드당 새 소켓, (2) n3-proxy ↔ n3-storage 소켓을 약 **1 GB 전송 후 재활용**  
  - **메모리 누수**: 초기 출시 후 n3-proxy 메모리 꾸준히 증가  
    - `jemalloc` 프로파일링으로 연결당 `hyper` **BytesMut** 버퍼에서 증가 확인  
    - 일부 클라이언트 연결이 **전송 중 정지**하고 정리되지 않아 버퍼가 남아 메모리 계속 증가  
    - **수정**: 소켓을 짧은 수명으로 만들고 시간 제한 적용  
      - **Keep-alive 비활성화**: 각 업로드 완료 후 연결 즉시 종료  
      - **타임아웃 강화**: 헤더/소켓 타임아웃 설정으로 정지된 업로드 종료 및 버퍼 해제  
  
### 스토리지  
- ## 인메모리 스토리지  
  - 가장 단순한 경로로 시작: **인메모리 스토리지**로 I/O 튜닝 회피, 직관적인 데이터 구조 사용  
  - `Arc<DashMap<Ulid, Bytes>>`로 비디오 저장, 각 비디오 업로드는 `bytes_used` 증가, 각 다운로드는 비디오 삭제 및 감소  
  - 용량의 **약 80% 이상**에서 업로드 거부 시작하여 OOM 회피, n3-proxy에 업로드 URL 서명 중지 신호  
  - `control` 핸들로 **업로드 및 가비지 컬렉션 수동 일시 중지** 가능  
- ## 우아한 재시작  
  - 메모리 전용 스토리지로 인해 재시작 시 진행 중 데이터 드롭 방지 필요  
  - **우아한 재시작 프로세스**  
    - 파드에 `SIGTERM`(StatefulSet이 한 번에 하나씩 롤링)  
    - 파드가 **Not Ready** 상태가 되고 Service에서 나감(새 업로드 없음)  
    - 이미 업로드된 비디오에 대한 다운로드 계속 제공  
    - 다운로드가 정지하면(최근 읽기 없음 → 처리 드레인)  
    - 열린 요청 완료 대기  
    - 재시작 후 다음 파드로 이동  
  - 정상 작동 시 파드는 수 초 내에 드레인  
- ## GC  
  - **두 가지 정리 메커니즘 사용**  
    - **다운로드 시 삭제**: 다운로드 직후 비디오 삭제, PoC에서 **재다운로드 제로** 확인, 비디오 프로세서가 내부적으로 재시도하므로 데이터 보유나 "처리됨" 상태 추적 불필요  
    - **낙오자를 위한 TTL GC**: 다운로드 시 삭제는 프로세서가 **건너뛴** 세그먼트(다운로드되지 않음 → 삭제되지 않음)를 커버하지 못함  
      - 경량 **TTL GC** 추가: 주기적으로 인메모리 **DashMap** 스캔 및 구성 가능한 임계값(예: 수 시간)보다 오래된 항목 제거  
  - **유지보수 모드**: 계획된 처리 다운타임 동안 내부 제어를 통해 **GC 일시 중지** 가능하여 소비가 중지된 동안 비디오 삭제 방지  
  
### 결론  
- ## 주요 성과  
  - S3를 폴백 버퍼로, N3를 주요 착지 영역으로 사용하여 **연간 약 50만 달러의 비용 절감**을 달성하면서 시스템을 단순하고 신뢰성 있게 유지  
  - **핵심 인사이트**: 대부분의 "구축 vs 구매" 결정은 기능에 초점을 맞추지만 **규모에서는 경제성이 계산을 바꿈**  
    - **짧은 수명 객체**(정상 작동 시 약 2초)에는 복제나 정교한 내구성 불필요, **단순한 인메모리 저장소로 작동**  
    - 처리가 지연되거나 유지보수가 객체 수명을 연장할 때는 **S3의 신뢰성 보장** 필요  
    - **양쪽의 장점**: N3가 정상 경로를 효율적으로 처리, S3가 객체가 더 오래 살아야 할 때 내구성 제공  
    - N3에 문제가 있으면(메모리 압박, 파드 충돌, 클러스터 문제) 업로드가 **S3로 원활하게 장애 조치**  
- ## 성공 요인  
  - 문제를 명확하게 사전 정의: **제약 조건, 가정, 경계**가 범위 확대 방지  
  - 미러 모드 PoC로 조기 검증: 병목 현상(TLS, 네트워크 스로틀링) 발견 및 커밋 전 가정 검증  
    - **과도한 엔지니어링 및 백트래킹 방지**  
- ## 언제 이런 것을 구축해야 하는가  
  - **충분한 규모**로 의미 있는 비용 절감이 가능하고, **단순한 솔루션을 가능하게 하는 특정 제약 조건**이 모두 있을 때 맞춤형 인프라 고려  
  - 시스템을 구축하고 유지보수하는 **엔지니어링 노력이 제거되는 인프라 비용보다 적어야** 함  
  - Nanit의 경우, 특정 요구사항(임시 저장소, 손실 허용성, S3 폴백)이 **유지보수 비용을 낮게 유지할 만큼 단순한 것을 구축** 가능하게 함  
  - 두 요인이 모두 없으면 관리형 서비스 고수  
  - **다시 할 것인가?** 예, 시스템이 프로덕션에서 안정적으로 실행 중이며, 폴백 설계로 **신뢰성을 희생하지 않고 복잡성 회피** 가능

## Comments



### Comment 45546

- Author: click
- Created: 2025-10-28T15:08:09+09:00
- Points: 2

그냥 ec2나 eks pod가 직접 비디오를 업로드받아서 처리하면 안됐을까 궁금하네요  
proxy까지 만들 정도였으면 pod 부하에 따른 eks 오토스케일링도 충분히 가능해보이는데 말이죠  
비디오 처리는 보통 인메모리에 파일을 통채로 올릴 필요가 없는데 각 인스턴스의 로컬 SSD에다가 임시파일 만들고 처리했으면 s3 폴백도 필요없었을듯한 느낌

### Comment 45541

- Author: t7vonn
- Created: 2025-10-28T11:18:57+09:00
- Points: 2

서버리스와 s3를 잘못 쓴 예시같네요  
그런데 해결책도 더 이상한 것 같습니다

### Comment 45519

- Author: neo
- Created: 2025-10-27T23:34:51+09:00
- Points: 2

###### [Hacker News 의견](https://news.ycombinator.com/item?id=45715204) 
- 정말 유익한 글이었음. 이런 **기술적 접근 과정**을 공유해주는 게 너무 좋음  
  내가 직접 같은 문제를 겪지 않더라도, 어떤 사고방식으로 접근했는지 보는 것만으로도 배움이 많았음  

- 솔직히 말하면, 이건 처음부터 **serverless를 쓰지 않았으면** 훨씬 깔끔했을 것 같음  
  수 초짜리 데이터를 억지로 AWS serverless 패러다임에 끼워 넣으려다 불필요한 비용과 복잡성이 생긴 느낌임  
  그래도 메모리 기반 솔루션으로 옮긴 건 좋은 선택이었음  
  - 결국 네트워크 처리량과 RAM 확보를 위해 무거운 인스턴스를 돌리게 된 셈인데, CPU는 거의 안 쓰고 있음  
    TLS handshake가 CPU를 많이 쓴다고 했지만, 그게 주요 병목일 것 같진 않음  
    그래도 이런 **워크플로우 맞춤형 시스템 설계**를 시도하는 점은 흥미로웠음  

- 사실 제목처럼 “S3를 직접 구현했다”기보다는, **S3 앞단에 메모리 캐시를 둔 구조**였음  
  멋지긴 하지만 완전한 자체 S3 대체는 아님  
  - 맞음, 그래도 기존 클라이언트를 바꾸기 어려우니 S3 API를 흉내 내야 했던 것 같음  
    제목이 뭐든 간에 흥미로운 프로젝트였음  
  - 왜 굳이 메모리 캐시여야 했는지는 이해가 안 됐음. 로컬 스토리지로도 충분했을 듯함  

- HN 스타일로 말하자면, Nanit이라는 회사 자체에 대해 이야기하고 싶음  
  Nanit은 **클라우드 기반 아기 모니터 카메라**를 운영함. 모든 영상과 오디오가 E2EE 없이 업로드됨  
  하드웨어는 비싸고, 구독 없이는 거의 쓸 수 없음. 게다가 $200짜리 스탠드를 사야 수면 추적 기능이 열림  
  이런 구조가 결국 **클라우드 종속 모델**을 강화하는 게 아쉬움  
  그래도 이번 글처럼 S3 의존을 줄이고 자체 스토리지로 옮긴 건 잘한 일임  
  - 나는 만족한 고객임. Nanit을 고른 이유는 단순히 “잘 작동했기 때문”임  
    다른 제품들은 앱이 불안정했음. 로컬 우선 + E2EE 솔루션이 있으면 좋겠지만, 현실적으로 **사용성**이 더 중요했음  
  - E2EE 관련해서, 이 경우엔 단순 저장이 아니라 **클라우드에서 영상 분석을 수행**하므로 E2EE 자체가 불가능함  
    진짜 E2EE를 원한다면 로컬에서 분석을 하고 결과만 업로드해야 함  
  - 나는 직접 **나무로 거치대**를 만들어 썼는데, 당시엔 소프트웨어 제한이 없었음. 지금은 바뀐 건지 궁금함  
  - “셀프 호스팅 영상은 어렵지 않다”는 말엔 동의 못함. 일반 사용자 입장에선 전혀 고려 대상이 아님  
  - 사실 아기 모니터에 클라우드가 꼭 필요한지도 모르겠음. 어차피 근처에 신뢰할 수 있는 어른이 있음  

- 이 글은 마치 스스로 문제를 만든 뒤 해결했다고 자축하는 느낌이었음  
  차라리 처음부터 **로컬 저장 하드웨어**를 팔았다면 단순하고 저렴했을 것임  
  클라우드 중심 설계는 이제 **2015년식 접근** 같음  

- 글은 훌륭했지만, ‘delete on read’를 S3에서 구현했을 때의 **비용 절감 효과**도 궁금했음  
  S3가 초 단위 과금이라면 절약폭이 꽤 컸을 수도 있음  
  또 이 솔루션은 사실상 S3의 ‘reduced redundancy’ 옵션과 유사함  

- $50만 절감했다고 하는데, 전체 비용이 얼마였는지 모르겠음  
  그게 $50만 중 $50만1달러인지, $5천5백만 달러 중 $50만인지에 따라 의미가 달라짐  
  - 맞음, 사실 이런 문제는 기술보다 **AWS와의 가격 협상(PPA)** 으로도 해결 가능함  
  - 그래도 S3는 여전히 **가성비 좋은 서비스**라서, 우리도 AWS를 떠나더라도 S3와 SQS는 유지할 예정임  

- 처음부터 **잘못된 아키텍처**를 선택한 뒤, 캐시로 덧칠한 느낌임  
  평균 2초짜리 영상을 S3에 올릴 이유는 중복 저장 외엔 없음  
  그냥 서버에서 직접 처리했다면 S3, SQS, Lambda 모두 없앨 수 있었을 것임  
  - 맞음, S3는 저장용이지 처리용이 아님  
    이렇게 단순한 문제를 왜 이렇게 복잡하게 만든 건지 모르겠음  

- “**앱 개발에 집중하고 인프라는 단순화하라**”는 고전적인 교훈 같음  
  차라리 캐시를 비디오 처리 서버 안에 직접 넣는 게 더 나았을 것임  

- 제목은 차라리 “**S3를 잘못 썼다**”가 더 정확했을 듯함  
  - 진짜로, 왜 수백만 개의 짧은 임시 파일을 S3에 저장하려 했는지 이해가 안 됨  
    결국 자체 메모리 스토어를 만들었는데, 차라리 **Redis** 같은 걸 썼으면 됐을 일임  
    직접 만든 시스템이 다운되면 영상은 사라지는 건가?  
    처음부터 Kinesis나 SQS로 보냈으면 훨씬 나았을 것임
