16P by GN⁺ 1일전 | ★ favorite | 댓글 3개
  • 나니트(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 폴백)이 유지보수 비용을 낮게 유지할 만큼 단순한 것을 구축 가능하게 함
    • 두 요인이 모두 없으면 관리형 서비스 고수
    • 다시 할 것인가? 예, 시스템이 프로덕션에서 안정적으로 실행 중이며, 폴백 설계로 신뢰성을 희생하지 않고 복잡성 회피 가능

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

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

Hacker News 의견
  • 정말 유익한 글이었음. 이런 기술적 접근 과정을 공유해주는 게 너무 좋음
    내가 직접 같은 문제를 겪지 않더라도, 어떤 사고방식으로 접근했는지 보는 것만으로도 배움이 많았음

  • 솔직히 말하면, 이건 처음부터 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로 보냈으면 훨씬 나았을 것임