본문으로 건너뛰기
Community실험 리포트

Qdrant 컨테이너가 새벽에 OOM Killed로 죽은 원인과 양자화로 메모리 1/4 줄인 기록

김혜진

@ops_hye

새벽에 벡터 검색이 통째로 먹통이 됐다. 원인 추적부터 복구까지 기록으로 남긴다.

증상은 단순했다. Qdrant self-host 인스턴스에서 검색 API가 503을 뱉기 시작하더니 컨테이너 자체가 사라져 있었다. docker logs에 아무것도 안 남아서 dmesg를 봤더니 이 한 줄이 범인이었다.

[123456.789] Out of memory: Killed process 4821 (qdrant)
        total-vm:9.2GB, anon-rss:3.7GB
oom_kill_constraint=CONSTRAINT_MEMCG

OOM Killer가 데려갔다. 4GB 인스턴스인데 total-vm이 9.2GB까지 올라가 있었다.

지금 보면 너무 당연한 산수인데 그날 새벽엔 안 보였다. 문서 임베딩이 약 200만 개, 차원 1024. raw 벡터만 잡아도 이렇게 된다.

2,000,000 × 1024 × 4byte(f32) ≈ 8.2GB

8GB짜리를 4GB에 욱여넣고 있었던 것이다. 평소엔 mmap 캐싱으로 버티다가, 하필 그 시간에 배치 재색인 잡이 돌면서 메모리가 튀었고 HNSW 그래프까지 같이 올라오니 한도를 넘겼다. HNSW 인덱스가 메모리를 먹는 건 알았지만 양을 가늠하지 않은 게 컸다.

급한 불은 인스턴스를 키워서 껐고, 그다음에 한 게 scalar quantization(int8) 적용이다. 케이스마다 다르겠지만 우리 데이터에선 메모리가 대략 1/4로 떨어졌고, recall은 200개를 수동으로 찍어봤을 때 거의 차이를 못 느꼈다. 검색이 살짝 느려지는 건 감수했다.

quantization_config:
  scalar:
    type: int8
    always_ram: true   # 양자화 벡터는 RAM, 원본은 디스크

그리고 재색인 잡을 라이브 검색과 같은 인스턴스에서 돌리던 걸 분리했다. 이게 사실 OOM의 진짜 방아쇠였다. 검색 트래픽을 받는 노드에서 무거운 색인을 같이 굴리면 둘 다 같이 죽는다. PoC 구성이 그대로 프로덕션으로 넘어와 고쳐지지 않은 자리였다.

중간에 Pinecone 같은 관리형으로 갈아탈지 비용을 잠깐 비교했다. 우리 규모(쿼리량이 그렇게 많지 않음)에선 self-host가 아직 한참 쌌다. 다만 새벽에 깨서 컨테이너를 살리는 시간을 사람값으로 환산하면 그 차이가 애매해진다. 결국 트래픽과 운영 인력 여유에 달린 문제라 단정은 못 하겠다.

지금은 양자화를 켜고, 메모리가 넉넉한 노드로 옮기고, 재색인을 분리하고, RSS가 80%를 넘으면 슬랙 알림이 가게 걸어뒀다.

교훈은 한 줄짜리 곱셈이다. 벡터 개수 × 차원 × 4byte. 이걸 PoC 때 냅킨에라도 해뒀으면 새벽을 날리지 않았을 것이다.

이 글에 대한 Q&A

이 글에 질문하기

이 글에서 궁금한 점이 있다면 먼저 질문을 남겨보세요.

댓글

첫 댓글을 남겨보세요.