ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 벡터 DB 온디맨드 관리 — Qdrant와 임베딩 서버의 cold start 실측
    IT 2026. 4. 24. 21:00
    벡터 DB 온디맨드 관리 — Qdrant와 임베딩 서버의 cold start 실측

    왜 벡터 DB를 상시 띄워둘 필요가 없는가

    로컬 RAG 시스템을 운영하다 보면 Qdrant 같은 벡터 DB를 Docker 컨테이너로 띄워두게 된다. 검색할 때, 인덱싱할 때, 야간 배치 작업에서 모두 Qdrant가 필요하니 "그냥 항상 켜두자"가 자연스러운 선택이다.

    그런데 실제 사용 패턴을 보면, Qdrant에 요청이 들어오는 건 하루에 몇 번 정도다. 검색 한 번, 야간 인덱싱 한 번. 나머지 23시간 동안 컨테이너는 메모리만 차지한 채 아무것도 하지 않는다.

    통합 메모리 아키텍처(DGX Spark처럼 CPU와 GPU가 메모리를 공유하는 환경)에서는 이 문제가 더 부각된다. Qdrant가 잡고 있는 메모리가 GPU 작업에 쓸 수 있는 메모리를 줄이기 때문이다. 25,000개 벡터(4096차원) 정도의 컬렉션이면 컨테이너 자체가 수백 MB를 차지하고, 이 메모리는 LLM 추론이나 임베딩 생성에 쓸 수 있는 자원이다.

    Qdrant만 올리면 되는 게 아니다

    벡터 DB의 온디맨드 관리를 처음 설계할 때, Qdrant Docker 컨테이너만 관리하면 된다고 생각하기 쉽다. 하지만 RAG 검색 파이프라인에는 Qdrant 외에 임베딩 서버도 필요하다. 검색 쿼리를 벡터로 변환해야 Qdrant에서 유사 벡터를 찾을 수 있기 때문이다.

    실제 검색 파이프라인은 이렇게 동작한다:

    1. 사용자 쿼리 → 임베딩 서버가 텍스트를 4096차원 벡터로 변환
    2. 변환된 벡터 → Qdrant에서 유사 벡터 검색
    3. 결과 반환

    Qdrant만 올려봤자, 임베딩 서버가 없으면 1번 단계에서 멈춘다. 온디맨드 관리의 범위는 Qdrant 컨테이너뿐 아니라 임베딩 서버까지 포함해야 한다.

    구현: ensure_qdrant()가 두 가지를 모두 관리

    RAG 시스템의 모든 경로가 get_client() 함수를 거친다. 여기서 호출하는 ensure_qdrant()가 Qdrant 컨테이너와 임베딩 서버를 한꺼번에 관리한다.

    def ensure_qdrant(timeout: float = 10.0) -> None:
        """Qdrant Docker 컨테이너와 임베딩 서버를 준비한다."""
        # 1) Qdrant 컨테이너
        result = subprocess.run(
            ["docker", "inspect", "-f", "{{.State.Running}}", "qdrant"],
            capture_output=True, text=True,
        )
        if result.stdout.strip() != "true":
            subprocess.run(["docker", "start", "qdrant"], check=True)
            # health check 폴링...
        STAMP_FILE.touch()
    
        # 2) 임베딩 서버 (best-effort)
        _ensure_embedding_server()
    
    def get_client() -> QdrantClient:
        ensure_qdrant()
        return QdrantClient(url=QDRANT_URL)

    임베딩 서버 관리는 best-effort 방식이다. vLLM 컨테이너가 떠있으면 그 안에서 임베딩 서버를 시작하고, 없으면 건너뛴다. 임베딩 서버 시작에 실패해도 Qdrant 자체는 사용 가능하므로, 인덱싱 같은 쓰기 작업은 영향을 받지 않는다.

    자동 종료는 cron + 셸 스크립트로 처리한다. 15분마다 stamp 파일의 수정 시각을 확인하고, 30분 이상 접근이 없으면 컨테이너를 내린다. Docker restart policy를 no로 변경해서 시스템 재부팅 시 자동 시작도 막는다.

    실측: 진짜 병목은 어디인가

    Qdrant 컨테이너가 완전히 정지된 상태에서, 임베딩 서버의 상태에 따라 두 가지 시나리오를 측정했다.

    시나리오 1: 임베딩 서버가 이미 떠있을 때

    단계 소요 시간
    Qdrant Docker 시작 + health check 1.12초
    쿼리 임베딩 생성 0.01초
    벡터 검색 (25,755 포인트) 0.03초
    합계 ~1.2초

    Qdrant만 내려가 있던 상황이라면 약 1초면 검색이 완료된다. 체감 지연이 거의 없는 수준이다.

    시나리오 2: 임베딩 서버도 내려가 있을 때

    단계 소요 시간
    임베딩 모델 로딩 (Qwen3-Embedding 8B, GPU) 61.82초
    Qdrant Docker 시작 + health check 1.12초
    쿼리 임베딩 생성 0.01초
    벡터 검색 (25,755 포인트) 0.03초
    합계 ~63초

    임베딩 모델(8B 파라미터)을 GPU에 올리는 데 약 1분이 걸린다. Qdrant의 1초와는 차원이 다른 비용이다. 실제 병목은 벡터 DB가 아니라 임베딩 서버라는 점이 명확해진다.

    통합 메모리 환경의 현실적 제약

    DGX Spark처럼 CPU와 GPU가 128GB 메모리를 공유하는 환경에서는 추가 제약이 있다. LLM 챗봇(Gemma 26B)이 GPU 메모리 ~84GB를 점유하고 있으면, 임베딩 모델(~15GB)을 함께 올릴 수 있는지가 관건이다.

    실측 결과 GPU 메모리 할당을 70%로 설정했을 때 두 모델이 공존 가능했다. 하지만 이는 환경에 따라 다를 수 있고, 메모리 압박이 심해지면 한쪽이 OOM으로 죽을 수 있다. 이런 경우에는 LLM과 임베딩 서버를 시간 분할로 운영해야 한다 — 검색이 필요할 때 LLM을 내리고 임베딩을 올리는 방식이다.

    정리: 무엇을 관리해야 하는가

    컴포넌트 cold start 관리 방식
    Qdrant Docker ~1초 ensure_qdrant() + cron idle-stop
    임베딩 서버 (warm) 0초 ensure_qdrant()가 자동 확인
    임베딩 서버 (cold) ~62초 ensure_qdrant()가 vLLM 컨테이너 안에서 자동 시작

    처음에는 Qdrant 컨테이너만 관리하면 된다고 생각했지만, 실측 결과 전체 파이프라인의 cold start는 임베딩 서버에 의해 결정된다. ensure_qdrant() 하나가 Qdrant 컨테이너와 임베딩 서버를 모두 관리하도록 설계한 덕분에, 호출하는 쪽에서는 인프라 상태를 신경 쓸 필요가 없다. 검색이든 인덱싱이든 get_client()만 부르면 필요한 모든 것이 자동으로 준비된다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.