Published on

Milvus 인덱스 빌드 느림·메모리 폭증 해결 7가지

Authors

Milvus를 운영하다 보면 create_index가 끝나지 않거나(수십 분~수 시간), 빌드 중 메모리가 급격히 치솟아 OOM으로 죽는 문제가 반복됩니다. 이 현상은 단순히 “데이터가 커서”라기보다, 세그먼트 구조·인덱스 타입·빌드 병렬성·메모리 오버헤드·컴팩션 상태가 서로 맞물릴 때 발생합니다.

이 글은 원인을 빠르게 좁히는 관찰 포인트와 함께, 인덱스 빌드 느림·메모리 폭증을 줄이는 7가지 처방을 제공합니다. (특히 IVF 계열과 HNSW에서 체감이 큽니다.)

관련 튜닝은 아래 글도 함께 보면 연결이 좋습니다.


먼저: “느림”과 “메모리 폭증”의 대표 원인 지도

인덱스 빌드는 대략 다음 단계를 거치며, 각 단계에서 병목이 달라집니다.

  1. 세그먼트 로딩: 원본 벡터/스칼라 필드 데이터를 읽어 빌드 프로세스가 접근 가능한 상태로 만듭니다.
  2. 학습 또는 그래프 구성: IVF는 nlist 중심으로 학습/클러스터링, HNSW는 M, efConstruction에 따라 그래프를 구성합니다.
  3. 인덱스 파일 생성 및 업로드: 로컬 디스크에 생성 후 오브젝트 스토리지로 업로드(또는 로컬/분산 스토리지 저장).
  4. 메타 업데이트 및 로드 전환: QueryNode가 새 인덱스를 로드하면서 검색 경로가 바뀝니다.

여기서 메모리 폭증은 보통 다음 패턴으로 발생합니다.

  • 빌드 중 원본 데이터 + 작업 버퍼 + 인덱스 구조가 동시에 메모리에 존재
  • 세그먼트가 너무 작거나 너무 많아 동시에 빌드되는 단위가 과도
  • HNSW 파라미터가 과격해 그래프가 비대해짐
  • IVF nlist가 데이터 규모 대비 과도해 centroid/리스트 관리 비용 증가

이제 해결책을 “즉시 효과가 큰 것”부터 정리합니다.


1) 세그먼트 크기부터 정상화: 작은 세그먼트가 빌드를 망친다

Milvus는 내부적으로 데이터를 세그먼트 단위로 관리합니다. 세그먼트가 지나치게 작으면(예: 수만 벡터 단위로 쪼개짐) 인덱스 빌드가 다음 이유로 급격히 비효율적이 됩니다.

  • 세그먼트마다 빌드 오버헤드가 반복(초기화/메타/IO)
  • 동시에 빌드되는 세그먼트 수가 늘며 메모리 피크가 커짐
  • 오브젝트 스토리지 업로드 파일이 잘게 쪼개져 IO 병목

체크 포인트

  • 컬렉션에 세그먼트가 과도하게 많지 않은가
  • flush 주기가 너무 짧지 않은가
  • compaction이 밀려 “작은 세그먼트 더미”가 쌓이지 않았나

처방

  • flush/segment 정책을 조정해 세그먼트가 너무 잘게 쪼개지지 않게 합니다.
  • 이미 쪼개졌다면 compaction을 먼저 정상화한 뒤 인덱스를 빌드합니다.

운영에서는 “인덱스 빌드 전에 compaction부터”가 의외로 가장 큰 효과를 줍니다. 인덱스는 세그먼트별로 만들어지므로, compaction으로 세그먼트 수를 줄이면 빌드 횟수 자체가 줄어듭니다.


2) 빌드 동시성(병렬성)을 제한해 메모리 피크를 낮춘다

인덱스 빌드는 CPU도 쓰지만, **메모리 피크는 ‘동시에 몇 개를 빌드하느냐’**에 크게 좌우됩니다. 특히 Kubernetes에서 노드 메모리가 충분해도 Pod limit에 걸려 OOM이 나는 경우가 많습니다.

체크 포인트

  • 인덱스 빌드 시점에 QueryNode/DataNode/IndexNode 중 누가 OOM 나는가
  • Pod 메모리 limit이 빌드 피크를 감당하는가
  • 동시에 여러 컬렉션/파티션에 인덱스를 걸고 있지 않은가

처방

  • IndexNode의 빌드 동시성을 낮추고, 대신 빌드 시간을 받아들여 안정성을 확보합니다.
  • 야간 배치로 “한 컬렉션씩” 인덱스 빌드하도록 오케스트레이션합니다.

실무 팁은 “빌드가 느려도 죽지 않게” 만드는 것입니다. 죽어서 재시도하면 총 소요시간은 오히려 늘고, 오브젝트 스토리지에 불완전 산출물이 쌓여 관리가 더 어려워집니다.


3) IVF 계열: nlist를 데이터 규모에 맞추지 않으면 학습·메모리가 터진다

IVF(IVF_FLAT, IVF_PQ, IVF_SQ8)에서 nlist클러스터 수입니다. nlist를 크게 잡으면 검색에서 nprobe 조절로 리콜/지연을 이쁘게 만들 수 있지만, 빌드 단계에서 비용이 급증합니다.

  • 학습(centroid 추정) 비용 증가
  • 리스트/메타 관리 비용 증가
  • 세그먼트가 많을수록 “세그먼트별 IVF”의 총비용이 커짐

경험칙(출발점)

  • 데이터가 N개일 때, nlist는 대략 sqrt(N) 근방에서 시작해 조정하는 경우가 많습니다.
  • 단, 차원 dim, 분포, 세그먼트 크기, 하드웨어에 따라 달라집니다.

코드 예시: IVF_FLAT 인덱스 생성

from pymilvus import connections, Collection

connections.connect(uri="http://localhost:19530")
col = Collection("my_vectors")

index_params = {
  "metric_type": "COSINE",
  "index_type": "IVF_FLAT",
  "params": {"nlist": 2048}
}

col.create_index(field_name="embedding", index_params=index_params)

nlist를 무작정 올리기 전에, 먼저 보수적으로 시작해 빌드 안정성을 확보하고, 검색 튜닝은 nprobe로 풀어가는 접근이 안전합니다. 검색 튜닝의 큰 흐름은 Milvus IVF_FLAT·HNSW 튜닝으로 지연 50% 줄이기에 더 자세히 정리해두었습니다.


4) HNSW: MefConstruction이 메모리 폭증의 1순위

HNSW는 빌드 시 그래프를 구성합니다. 이때 M(노드당 링크 수)과 efConstruction(구성 품질/탐색 폭)이 커지면:

  • 빌드 시간이 증가
  • 그래프 엣지 수가 늘어 메모리 사용량이 크게 증가
  • 특히 고차원·대규모 데이터에서 피크가 가파르게 상승

처방

  • 초기에는 MefConstruction을 보수적으로 설정해 빌드 안정성을 확보합니다.
  • 리콜이 부족하면 검색 시 ef를 올리는 방식으로 먼저 보완합니다.

코드 예시: HNSW 인덱스 생성

from pymilvus import Collection

col = Collection("my_vectors")

index_params = {
  "metric_type": "IP",
  "index_type": "HNSW",
  "params": {
    "M": 16,
    "efConstruction": 128
  }
}

col.create_index(field_name="embedding", index_params=index_params)

운영에서 자주 보는 실수는 “리콜이 안 나오니 efConstruction부터 크게 올리는 것”입니다. 그 결과 빌드가 느려지고 메모리가 터집니다. 리콜은 먼저 **검색 파라미터(ef)**로 조정하고, 인덱스 빌드 파라미터는 마지막에 만지는 편이 안전합니다.


5) 인덱스 타입을 ‘빌드 비용’ 관점에서 재선정한다

인덱스 선택은 검색 지연만이 아니라 빌드 시간·빌드 메모리·저장 공간의 트레이드오프입니다.

  • IVF_FLAT: 빌드는 상대적으로 단순하지만, nlist가 과하면 학습/메모리 부담 증가
  • IVF_PQ/IVF_SQ8: 검색 메모리를 줄이지만, 빌드 과정이 더 무겁고 파라미터에 민감
  • HNSW: 쿼리 성능이 좋지만, 빌드 메모리 피크가 커지기 쉬움

처방

  • “빌드가 너무 무겁다”면, 일단 IVF_FLAT로 안정화한 뒤 점진적으로 IVF_PQ 또는 HNSW로 이동합니다.
  • 데이터가 계속 유입되는 환경이라면, 증분 빌드/주기적 재빌드 전략까지 포함해 선택해야 합니다.

하이브리드 검색(벡터+BM25/필터)까지 엮이면 인덱스 선택 기준이 더 복잡해지는데, 그 경우 Pinecone·Milvus 하이브리드검색 튜닝 7가지에서 “필터가 IVF/HNSW에 미치는 영향” 관점이 도움이 됩니다.


6) 메모리 ‘총량’이 아니라 ‘피크’를 줄여라: 빌드/로드 타이밍 분리

현장에서 자주 발생하는 케이스는 이겁니다.

  • IndexNode가 인덱스를 빌드하며 메모리를 많이 씀
  • 동시에 QueryNode가 세그먼트를 로드/리밸런싱하며 메모리를 또 씀
  • 둘이 겹치는 순간 피크가 튀어 OOM

처방

  • 인덱스 빌드 시간대에 로드/리밸런싱/대량 ingest를 피합니다.
  • 가능하면 “빌드 전용 시간창”을 만들고, 트래픽이 낮을 때 수행합니다.
  • 컬렉션을 여러 개 동시에 재인덱싱하지 말고 큐로 직렬화합니다.

Kubernetes라면 OOM 이후 CrashLoopBackOff로 이어지기 쉬우니, 증상 확인 및 빠른 원인 분리는 K8s CrashLoopBackOff 원인 10분 진단법 체크리스트가 그대로 적용됩니다.


7) “데이터 품질/형상”이 빌드를 느리게 한다: 차원·정규화·중복을 점검

인덱스 빌드는 데이터 분포의 영향을 받습니다. 특히 다음이 누적되면 빌드가 비정상적으로 느려지거나 메모리 사용이 증가할 수 있습니다.

  • 차원(dim)이 과도: 벡터 1개당 메모리/IO가 증가
  • 정규화 불일치: COSINE을 쓰면서 정규화를 안 했거나, IP/L2 선택이 데이터에 맞지 않아 분포가 나빠짐
  • 중복 벡터가 과다: 동일/유사 벡터가 대량이면 클러스터링/그래프 구성에서 비효율이 커질 수 있음

처방

  • 임베딩 파이프라인에서 차원 축소(예: PCA) 또는 모델 교체를 검토합니다.
  • COSINE이면 보통 L2 정규화를 일관되게 적용합니다.
  • 중복이 의심되면 샘플링해서 near-duplicate 비율을 측정하고, 중복 제거 후 인덱스를 재빌드합니다.

아래는 COSINE을 위한 간단한 정규화 예시입니다.

import numpy as np

def l2_normalize(v: np.ndarray, eps: float = 1e-12) -> np.ndarray:
    norm = np.linalg.norm(v)
    return v / (norm + eps)

vec = np.array([0.1, 0.2, 0.3], dtype=np.float32)
vec = l2_normalize(vec)

데이터 중복/조인 실수로 레코드가 폭증하는 형태라면(예: upstream에서 데이터가 예상보다 수배로 늘어남) 원인 규명이 먼저입니다. 벡터 DB 이전 단계에서 행 폭증이 발생하는 케이스는 Pandas merge에서 행 폭증? 중복키 진단법과 같은 “중복키 진단” 접근이 그대로 통합니다.


운영에서 바로 쓰는 점검 순서(권장)

인덱스 빌드가 느리거나 메모리가 터질 때, 아래 순서로 보면 시행착오가 줄어듭니다.

  1. 세그먼트 수/크기 확인, compaction 적체 해소
  2. 동시에 빌드되는 작업 수 제한(직렬화)
  3. 인덱스 파라미터 보수화: IVF는 nlist부터, HNSW는 M/efConstruction부터
  4. 빌드 시간대에 ingest/로드 이벤트 겹침 제거
  5. 데이터 차원/정규화/중복 점검
  6. 그래도 안 되면 인덱스 타입 재선정(빌드 비용 관점)

마무리: “검색 성능 튜닝”보다 먼저 “빌드 안정성”을 잡자

Milvus 인덱스 문제는 대부분 파라미터 하나로 해결되지 않습니다. **세그먼트 구조(컴팩션)와 빌드 동시성(피크 메모리)**을 먼저 안정화한 뒤, IVF/HNSW 파라미터를 조정하는 흐름이 가장 재현성이 좋습니다.

다음 단계로는 “빌드가 끝난 뒤 검색 지연/리콜을 어떻게 최적화할지”가 남습니다. 그 부분은 위에서 링크한 Milvus 튜닝 글을 기준으로, nprobe/ef를 중심으로 점진적으로 올리는 전략을 권합니다.