백만 벡터 서빙에 필요한 최소 RAM

백만 벡터 서빙에 필요한 최소 RAM (Minimal RAM you need to serve a million vectors)

프로세스의 메모리 소비를 측정할 때 우리는 흔히 htop 같은 도구에 기대요. 그게 얼마나 많은 RAM이 쓰이는지 알려 준다고 믿지만, 이 방법은 오해를 부를 수 있고 프로세스의 실제 메모리 사용량을 항상 정확히 반영하지는 못해요.

htop이 메모리 사용량의 신뢰할 만한 지표가 아닐 수 있는 이유는 여러 가지예요. 예를 들어 프로세스가 메모리를 미리 할당만 하고 사용하지 않거나, 해제된 메모리를 실제로 반환하지 않아 메모리 소비가 과대 계상될 수 있어요. 프로세스가 fork되면 별도의 메모리 공간을 갖지만 부모와 같은 코드와 데이터를 공유하는데, 그 경우 자식 프로세스의 메모리 소비가 두 번 계상돼요. 게다가 프로세스가 디스크 캐시를 활용하면 그것도 htop 측정에서 resident 메모리로 잡혀요.

결과적으로 htop이 프로세스가 10GB 메모리를 쓴다고 보여 줘도, 그 프로세스가 실제로 효율적으로 동작하기 위해 10GB RAM이 필요하다는 뜻은 아니에요. 이 글에서는 RAM 사용량을 제대로 측정하는 방법과, 최적의 메모리 소비를 위해 Qdrant를 최적화하는 방법을 살펴볼게요.

출처: 공식문서

실제 RAM 요구량 측정하기

프로그램을 돌리는 데 필요한 RAM이 얼마인지 추정하려면 메모리 소비를 알아야 해요. 그래서 이를 정하기 위해 간단한 실험을 할 수 있어요. 프로세스에 허용된 메모리를 제한하고, 어느 시점에서 멈추는지 관찰해 보는 거예요. 이렇게 하면 프로그램이 동작하는 데 필요한 최소 RAM을 알아낼 수 있어요.

한 가지 방법은 그리드 서치를 하는 거지만, 더 효율적인 방법은 이진 탐색으로 필요한 최소 RAM을 빠르게 찾는 거예요. 프로세스의 메모리 사용을 제한하려면 docker를 쓰면 됩니다.

각 벤치마크를 실행하기 전에 페이지 캐시를 다음 명령으로 비워 주는 게 중요해요.

sudo bash -c 'sync; echo 1 > /proc/sys/vm/drop_caches'

이렇게 하면 프로세스가 이전 실행의 데이터를 활용하지 않아, 더 정확하고 일관된 결과를 얻을 수 있어요.

1GB 메모리 제한으로 Qdrant를 실행하는 명령은 다음과 같아요.

docker run -it --rm \
    --memory 1024mb \
    --network=host \
    -v "$(pwd)/data/storage:/qdrant/storage" \
    qdrant/qdrant:latest

벤치마크를 실행해 볼게요

Qdrant가 1백만 벡터를 서빙하는 데 RAM이 얼마나 필요한지 알아보기 위해 몇 가지 벤치마크를 실행해 볼게요.

glove-100-angularvector-db-benchmark 프로젝트의 스크립트를 사용해서 벡터를 업로드하고 쿼리할 수 있어요. 첫 실행에서는 Qdrant의 기본 설정, 즉 모든 데이터를 RAM에 저장하는 설정을 사용할게요.

# Upload vectors
python run.py --engines qdrant-all-in-ram --datasets glove-100-angular

벡터를 업로드한 뒤, 메모리 소비와 검색 속도에 어떤 영향을 주는지 보려고 같은 실험을 다른 RAM 제한으로 반복할게요.

# Search vectors
python run.py --engines qdrant-all-in-ram --datasets glove-100-angular --skip-upload

전부 인메모리 (All in Memory)

첫 번째 실험에서는 모든 벡터를 메모리에 저장했을 때 시스템이 얼마나 잘 동작하는지 테스트했어요. 1512mb에서 1024mb까지 다양한 메모리 양으로 시도했고, 시스템이 처리할 수 있는 초당 요청 수(rps)를 측정했어요.

Memory Requests/s
1512mb 774.38
1256mb 760.63
1200mb 794.72
1152mb out of memory
1024mb out of memory

1152MB 메모리 제한에서는 시스템이 메모리 부족으로 실패했지만, 1512mb, 1256mb, 1200mb에서는 약 780 RPS를 처리할 수 있었어요. 이는 약 1백만 벡터를 서빙하려면 약 1.2GB 메모리가 필요하고, 1.2GB 위로 메모리를 제한해도 속도 저하가 없다는 걸 시사해요.

벡터를 MMAP으로 저장 (Vectors stored using MMAP)

조금 더 나아가 볼게요! 두 번째 실험에서는 벡터를 메모리 매핑 파일(mmap)로 저장했을 때 시스템이 얼마나 잘 동작하는지 테스트했어요. 컬렉션을 이렇게 만듭니다.

PUT /collections/benchmark
{
  "vectors": {
    ...
    "on_disk": true
  }
}

이 설정은 세그먼트 크기가 20000Kb(약 40K개의 128d-벡터)보다 클 때 Qdrant가 벡터에 mmap을 사용하도록 지시해요.

이제 메모리 부족은 600mb RAM만 허용할 때 발생해요.

실험 상세
Memory Requests/s
1200mb 759.94
1100mb 687.00
1000mb 10

--- 약간 더 빠른 디스크 사용 ---

Memory Requests/s
1000mb 25 rps
750mb 5 rps
625mb 2.5 rps
600mb out of memory

이 시점에서는 네트워크 마운트 스토리지에서 더 빠른 디스크로 전환해야 했어요. 네트워크 기반 스토리지는 시스템이 쿼리를 서빙하기 위해 필요한 순차 읽기 양을 감당하기엔 너무 느렸거든요.

하지만 먼저 1백만 벡터를 서빙하는 데 RAM이 얼마나 필요한지 보자고요. 그다음에 속도 최적화도 논의할게요.

벡터와 HNSW 그래프를 MMAP으로 저장

세 번째 실험에서는 벡터와 HNSW 그래프를 메모리 매핑 파일로 저장했을 때 시스템이 얼마나 잘 동작하는지 테스트했어요. 컬렉션을 이렇게 만듭니다.

PUT /collections/benchmark 
{
  "vectors": {
    ...
    "on_disk": true
  },
  "hnsw_config": {
    "on_disk": true
  },
  ...
}

이 설정으로 단 135mb의 RAM으로 1백만 벡터를 서빙할 수 있었어요!

실험 상세
Memory Requests/s
600mb 5 rps
300mb 0.9 rps / 1.1 sec per query
150mb 0.4 rps / 2.5 sec per query
135mb 0.33 rps / 3 sec per query
125mb out of memory

이 시점에서 디스크 속도의 중요성이 결정적이 되어요. 135mb의 RAM으로 검색 요청을 서빙할 수는 있지만, 요청 속도 때문에 시스템을 프로덕션에서 쓰는 건 불가능해요.

속도를 어떻게 개선할 수 있는지 볼게요.

검색 속도 높이기

디스크 파라미터가 검색 속도에 미치는 영향을 측정하기 위해 fio 도구로 여러 유형의 디스크 속도를 테스트했어요.

# Install fio
sudo apt-get install fio

# Run fio to check the random reads speed
fio --randrepeat=1 \
    --ioengine=libaio \
    --direct=1 \
    --gtod_reduce=1 \
    --name=fiotest \
    --filename=testfio \
    --bs=4k \
    --iodepth=64 \
    --size=8G \
    --readwrite=randread

처음에는 네트워크 마운트 디스크에서 테스트했는데, 성능이 너무 느렸어요. 읽기 IOPS 6366, 대역폭 24.9 MiB/s였죠.

read: IOPS=6366, BW=24.9MiB/s (26.1MB/s)(8192MiB/329424msec)

성능을 개선하기 위해 로컬 디스크로 전환했는데 훨씬 빨라졌어요. 읽기 IOPS 63.2k, 대역폭 247 MiB/s였죠.

read: IOPS=63.2k, BW=247MiB/s (259MB/s)(8192MiB/33207msec)

그 덕분에 상당한 속도 향상을 얻었지만, 더 개선할 수 있는지 보고 싶었어요. 그래서 로컬 SSD가 있는 머신으로 전환했고, 더 나은 결과를 보여 줬어요. 읽기 IOPS 183k, 대역폭 716 MiB/s였죠.

read: IOPS=183k, BW=716MiB/s (751MB/s)(8192MiB/11438msec)

이 결과들이 검색 속도로 어떻게 이어지는지 볼게요.

Memory RPS with IOPS=63.2k RPS with IOPS=183k
600mb 5 50
300mb 0.9 13
200mb 0.5 8
150mb 0.4 7

보시다시피 디스크 속도가 검색 속도에 상당한 영향을 줘요. 로컬 SSD를 쓰자 검색 속도를 10배나 높일 수 있었어요!

프로덕션급 디스크를 쓰면 검색 속도는 더 높을 수 있어요. 일부 SSD 구성은 1M IOPS 이상에 도달할 수 있죠.

이는 Qdrant에서 큰 데이터셋을 낮은 검색 지연 시간으로 서빙할 때 흥미로운 선택지가 될 수 있어요.

결론

이 글에서 Qdrant는 RAM 사용 측면에서 유연성이 있고 큰 데이터셋을 서빙하는 데 사용될 수 있음을 보여 줬어요. RAM 사용량과 검색 속도 사이의 조절 가능한 트레이드오프를 제공하죠. Qdrant에 대해 더 배우고 싶다면 오늘 데모를 예약해 보세요!

여러분이 프로젝트에서 Qdrant를 어떻게 쓰고 있는지, 어떤 어려움을 겪고 있는지, 우리가 어떻게 도울 수 있는지 듣고 싶어요. Discord에 참여해 경험을 공유해 주세요!

더 알아보기 (Learn more)