무료 티어 컨테이너에서 내 벡터 데이터베이스를 죽게 만든 원인
요약
실시간 날씨 데이터 대시보드 구축 중 무료 티어 컨테이너 환경에서 발생한 메모리 부족 문제를 다룹니다. ChromaDB와 같은 무거운 벡터 데이터베이스 대신 가벼운 인메모리 검색 방식으로 교체하여 해결한 사례를 공유합니다.
핵심 포인트
- 무료 티어의 제한된 자원 환경에서는 오버헤드가 큰 라이브러리 주의
- 데이터 규모가 작을 경우 무거운 벡터 DB 대신 인메모리 검색이 효율적
- ChromaDB 사용 시 발생한 OOM(Out-of-Memory) 문제 해결 과정
저는 실시간 공공 날씨 데이터를 수집하고, 모든 측정값을 이상 탐지기(anomaly detector)와 유사도 검색(similarity search)에 통과시킨 뒤, 결과가 발생하는 즉시 WebSockets를 통해 브라우저로 스트리밍하는 실시간 인사이트 대시보드를 구축했습니다. 이는 모의 데이터(mocked data) 대신 인증이 필요 없는 무료 공공 API를 사용하여 실시간 데이터 플랫폼 아키텍처(수집(ingestion), 스트리밍(streaming), AI 기반 패턴 탐지(AI-driven pattern detection), 벡터 검색(vector search))를 보여주는 작은 데모입니다.
라이브 데모: https://realtime-weather-insights-production.up.railway.app
코드: https://github.com/leovasone/realtime-weather-insights
사례 연구: https://vasone.com.br/realtime-insights.html
아키텍처 (Architecture)
Open-Meteo API → 폴링 루프 (poll loop) (60초) → 이상 탐지기 (anomaly detector) (z-score)
→ 벡터 저장소 (vector store) (인메모리 (in-memory)) → 유사도 검색 (similarity search)
→ 내레이터 (narrator) (Claude, 선택 사항)
...
비동기 루프(async loop)가 60초마다 몇몇 도시를 폴링(poll)합니다. 각 측정값은 이동 z-점수(rolling z-score) 이상 탐지기(도시별, 지표별 — 온도, 습도, 풍속, 기압, 구름 양)를 통과하며, 다른 도시에서 가장 유사한 과거 기록을 찾는 유사도 검색(similarity search)을 거칩니다. 결합된 결과는 모든 연결된 클라이언트에 브로드캐스트(broadcast)되며, 브라우저 측에서의 폴링(polling)은 발생하지 않습니다.
이 시스템이 자원이 제한된 호스트에서 실제 트래픽과 함께 실제로 실행되자 두 가지 실제 운영 문제가 나타났으며, 두 문제 모두 로컬 데모에서는 나타나지 않았을 것입니다.
문제 1: 벡터 데이터베이스가 내가 겪지도 않은 확장성 문제를 해결하려 하고 있었다
첫 번째 버전은 유사도 검색(similarity search)을 위해 ChromaDB를 사용했습니다. Railway의 무료 티어(free tier) 환경에서 운영할 때, 컨테이너가 약 60~70초마다 트레이스백(traceback) 없이 종료되고 재시작되었습니다. 이는 메모리 부족 종료(out-of-memory kill)와 일치하는 현상이었으며, 그때마다 모든 열려 있는 WebSocket 연결이 끊어졌습니다.
실제 데이터 양은 매우 적었습니다. 도시당 몇 개의 5차원 수치 벡터(numeric vectors)가 전부였습니다. 네이티브 바인딩(native bindings)을 포함한 완전한 벡터 데이터베이스(vector database)는 이 앱이 겪고 있지 않은 규모(scale) 문제를 해결하려 하고 있었습니다. 저는 이를 동일한 인터페이스 뒤에서 작동하는 브루트 포스(brute-force) 인메모리 검색(in-memory search)으로 교체했습니다. 외부 의존성(external dependency)도 없고, 네이티브 바인딩(native bindings)도 없으며, 작은 제한된 리스트(bounded list) 이상의 메모리 오버헤드(memory overhead)도 발생하지 않습니다.
이 교훈은 일반화될 수 있습니다. 리소스가 제한된 호스트(host)에서 벡터 데이터베이스(vector database)를 사용하기 전에, 실제 데이터 양에 대해 브루트 포스(brute-force) 검색이 이미 충분히 빠른지 확인해 볼 가치가 있습니다. N이 작을 경우 대개 충분히 빠르며, 이는 배포 위험(deployment risk)의 한 부류를 완전히 제거해 줍니다.
이슈 2: 잘못된 CDN URL 하나가 차트 그 이상을 무너뜨리다
Chart.js는 원래 단일 차단형(blocking) <script> 태그를 통해 로드되었습니다. 적어도 한 번의 실제 브라우저 세션에서 CDN URL의 대소문자 오타로 인해 로드에 조용히 실패했는데, 페이지 전체의 스크립트가 하나의 블록으로 실행되었기 때문에 그 단 한 번의 실패가 WebSocket 연결이 아예 수립되지 못하게 막았습니다. 차트 패널이 빈 상태로 보이는 것은 눈에 보이는 증상이었을 뿐, 진짜 버그는 관련 없는 서드파티(third-party) 스크립트의 실패가 전체 실시간 데이터 피드(live data feed)를 중단시킬 수 있다는 점이었습니다.
저는 로딩 방식을 결합되지 않도록(decoupled) 다시 작성했습니다. 하나의 CDN을 시도하고, 실패하면 두 번째로 폴백(fallback)하며, 둘 다 실패할 경우 빈 공간 대신 눈에 보이는 "차트를 사용할 수 없음" 메시지를 표시합니다. 이 중 어떤 것도 WebSocket 연결이나 도시별 카드(per-city cards)를 차단하지 않으며, 사실 이 카드들은 처음부터 Chart.js에 의존하지 않았습니다. 이 수정의 핵심은 차트 라이브러리에 관한 것이 아니라, 비임계적 의존성(non-critical dependency)의 실패가 임계적 기능(critical functionality)의 실패로 연쇄적으로 이어지지 않도록 보장하는 것이었습니다.
여기서 무엇이 실제로 "AI"인지에 대한 솔직한 고백
Z-score 이상 탐지 (anomaly detection) 및 벡터 거리 유사도 검색 (vector-distance similarity search)은 정당하고 유용한 기술이지만, 이는 통계학 및 선형 대수학 (linear algebra)이지 머신러닝 (machine learning) 모델이 아닙니다. 이 파이프라인에서 진정으로 생성형 AI (generative-AI) 요소인 부분은 선택 사항인 내레이터 (Claude Haiku)뿐이며, 이 내레이터는 모든 도시를 통틀어 60초 주기당 최대 한 번, 구조화된 이상 징후와 유사도 매칭 결과를 하나의 평이한 언어 문장으로 변환합니다. 도시당 한 번이 아니라 주기당 한 번 호출하는 이유는 비용을 무시할 수 있는 수준으로 유지하기 위함이며,
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기