백만 벡터 서빙에 필요한 최소 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-angular와 vector-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)
- Filterable HNSW 아티클 — HNSW 알고리즘과 필터링
- vector-db-benchmark 프로젝트 — 벤치마크 스크립트
- Qdrant 데모 예약