새벽 3시에 Qdrant 컨테이너가 Killed 한 줄 남기고 죽은 이야기
김혜진
@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 때 냅킨에라도 해뒀으면 새벽을 안 날렸을 텐데.