Lance 쿼리 모범 사례
이 가이드를 사용해 Lance 쿼리에 적합한 BE별 캐시 예산과 인덱스 세그먼트 구성을 선택하세요. 먼저 워크로드를 파악하고, 캐시에 유지해야 할 콘텐츠의 크기를 추정한 다음, 목표 동시 실행 수준에서 검증하세요. 예시는 계획을 세우기 위한 출발점이며, 벤치마크로 입증된 최적값이나 BE 메모리 한도가 아닙니다.
Lance Catalog는 실험적 기능이며 Apache Doris 4.2부터 지원됩니다. 먼저 Lance Catalog에 따라 접근을 구성하고 리더 호환성을 확인하세요. Doris는 기존 Lance 인덱스를 읽습니다. 인덱스 구축과 유지 관리에는 호환되는 Lance 도구를 사용하세요.
벡터 또는 전문 검색과 결합된 스칼라 필터의 필터 적용 위치, 정렬된 인덱스 구축, Profile 진단에 관해서는 Lance 하이브리드 검색 성능 모범 사례를 참조하세요.
1. 튜닝 경로 선택
| 워크로드 | 시작할 작업 | 검증할 항목 |
|---|---|---|
| 단일 쿼리 지연 시간이 짧아야 하는 벡터 검색 | 자주 탐색하는 벡터 파티션을 유지하고, 세그먼트 가이드를 사용해 적격 BE 수에 따른 균형 잡힌 세그먼트 수를 비교 | 비슷한 recall에서의 P95 지연 시간, 가장 느린 스캐너, 최대 메모리 사용량 |
| 동시 실행이 많은 벡터 검색 | 서로 다른 쿼리 벡터가 사용하는 hot 파티션의 합집합에 맞춰 예산을 잡고, 쿼리마다 Split 수를 최대화하기보다 동시 검색에 필요한 메모리와 CPU를 확보 | 목표 동시 실행 수준에서의 처리량과 P95 지연 시간, 캐시 재로드, BE별 메모리 사용량 |
| BTree 범위/지점 필터 또는 Bitmap/LabelList 필터 | 조회 구조를 포함해 hot 페이지 또는 비트맵 항목을 추정하고 조건식과 인덱스가 사용되는지 확인 | 페이지 로드, 선택도, 임시 row-ID 집합, 출력 열 읽기 |
| 전문 검색 또는 구문 검색 | 사전, 문서 메타데이터, posting, 활성화된 경우 위치 정보까지 추정하고, 워밍업에 다양한 검색어를 포함 | 일반적인 검색어의 쿼리, 구문 쿼리의 메모리 사용량, 후보 병합 |
| 같은 BE를 공유하는 벡터, 스칼라 및 전문 검색 쿼리 | 각 공유 작업 집합을 합산하고 함께 테스트 | 한 쿼리 계열이 다른 계열의 항목을 퇴출시키는지 여부 |
| 출력 열 가져오기 또는 스캔이 지배적인 경우 | 데이터 파일 읽기와 별도의 디스크 데이터 캐시를 확인 | 유효 바이트와 읽기 증폭의 비율, 디코딩 또는 데이터 가져오기가 지배적인지 여부 |
스칼라 필터가 벡터 쿼리의 Prefilter에 포함될 수도 있으므로, 단일 쿼리에 스칼라 인덱스와 벡터 인덱스의 콘텐츠가 모두 필요할 수 있습니다. 시나리오를 상호 배타적으로 취급하지 말고 해당하는 행을 모두 선택하세요.
2. 입력 정보 수집 및 메모리 예산 설정
각 BE 또는 실제 배치를 대표하는 BE에 대해 다음 정보를 수집하세요.
- 워크로드: 목표 QPS/동시 실행 수준, 지연 시간 목표, 필요한 recall,
top_k, 필터, 구문 쿼리, 출력 열. - 리소스: 쿼리를 실행할 수 있는 BE, CPU 용량, 다른 Doris 워크로드와 프로세스/OS 요구량을 제외하고 사용 가능한 메모리.
- 인덱스: 유형, 버전, 물리적 세그먼트 수, 커버리지, 벡터 차원/인코딩, IVF 파티션 수, 스칼라 값의 너비 또는 문자열 길이.
- 재사용: 대표적인 쿼리 기간에 접근하는 서로 다른 파티션, 페이지, 검색어 또는 비트맵 항목. 한 쿼리가 반환한 행이 아니라 여러 쿼리에 걸친 작업 집합을 사용하세요.
- 쿼리 최대 사용량: 목표 동시 실행 수준에서 활성 파티션 로드, Prefilter/결과 row-ID 집합, 후보 힙, 디코딩, refinement 및 두 번째 단계의 데이터 가져오기에 필요한 메모리.
인덱스 계산식으로 payload를 추정한 뒤 합산한 예산이 다음 범위에 드는지 확인하세요.
available_Lance_cache_budget_on_BE
= BE memory budget
- other BE memory needs
- peak concurrent query working memory
- safety headroom
index_cache_target + metadata_cache_target
<= available_Lance_cache_budget_on_BE
이는 계획 단계의 점검식이며 강제되는 메모리 예약량이 아닙니다. 캐시 용량은 쿼리 메모리나 프로세스 RSS의 상한이 아닙니다. 재사용 가능한 캐시 작업 집합에 동시 실행 쿼리 수를 곱하지 마세요. 대신 추가 쿼리 작업 메모리를 확보하고, 동시 실행 쿼리가 서로 다른 데이터에 접근한다면 캐시 콘텐츠의 합집합이 커지는 점을 반영하세요.
전체 파라미터 정의와 기본값은 BE 구성에 있습니다. 각 BE의 be.conf에서 lance_* BE 파라미터를 설정하고 해당 BE를 재시작하세요. 이 파라미터는 Catalog 속성이나 SQL 세션 변수가 아닙니다. 인덱스 세그먼트 수와 IVF 구축 파라미터는 이러한 캐시 파라미터가 아니라 Lance 인덱스 구축 도구를 통해 설정합니다.
3. 인덱스 작업 집합 추정
각 BE가 재사용해야 하는 인덱스 파티션에 맞춰 인덱스 캐시 크기를 정하세요. 스토리지에 있는 전체 인덱스, 캐시에 저장되는 작업 집합, 쿼리의 최대 메모리 사용량은 서로 다른 값입니다. 아래 추정치는 Doris가 지원하는 벡터 인덱스 유형 6개를 다룹니다. 정확한 캐시 사용량이나 메모리 한도가 아니라 계획용 추정치입니다.
인덱스 유형별 벡터 payload
다음 기호를 사용합니다. 모든 계산식의 결과 단위는 바이트이며, 1 GiB = 1073741824 bytes입니다.
| 기호 | 의미 |
|---|---|
N | 추정 대상 파티션에 있는 인덱싱된 벡터 수. 일반적인 단일 벡터 열에서는 인덱싱된 행 수입니다. 다중 벡터 열에서는 인덱싱된 하위 벡터를 세고 행 매핑과 refinement를 위한 예산을 별도로 확보합니다. |
D | 벡터 차원. |
w | 저장된 비양자화 벡터 요소당 바이트 수: Float16은 2, Float32는 4, Float64는 8입니다. 이진 uint8 벡터에서는 실제 바이트 길이를 D로, w=1로 사용합니다. |
m, b | PQ 하위 벡터 수와 하위 벡터 코드당 비트 수. 쿼리 파라미터가 아니라 인덱스 구축 설정입니다. |
G | 로드된 HNSW 그래프가 차지하는 메모리. 모든 레벨과 보조 구조를 포함합니다. |
벡터 코드만이 아니라 해당하는 모든 구성 요소를 더하세요. 첫 번째 표는 벡터별 payload를 구분하고, 다음 표들은 공유 구조와 HNSW 그래프 구성 요소를 나열합니다. 모든 계산식은 로드된 버퍼를 추정합니다. 측정으로 표시된 행에도 예산이 필요하며, 비용이 없는 항목이 아닙니다. 공유 할당은 한 번만 계산하세요.
| 인덱스 유형 | 벡터 값 / 코드 | UInt64 row ID | HNSW 그래프 | 추가로 필요한 예산 |
|---|---|---|---|---|
IVF_FLAT | N * D * w | 8 * N | 없음 | IVF 구조 + 아래의 공통 오버헤드 |
IVF_SQ | SQ8의 경우 N * D | 8 * N | 없음 | IVF 구조 + SQ 메타데이터 + 공통 오버헤드 |
IVF_PQ | N * ceil(m * b / 8) | 8 * N | 없음 | IVF 구조 + PQ 코드북 + 공통 오버헤드 |
IVF_HNSW_FLAT | N * D * w | 8 * N | G 추가 | IVF 구조 + 공통 오버헤드 |
IVF_HNSW_SQ | SQ8의 경우 N * D | 8 * N | G 추가 | IVF 구조 + SQ 메타데이터 + 공통 오버헤드 |
IVF_HNSW_PQ | N * ceil(m * b / 8) | 8 * N | G 추가 | IVF 구조 + PQ 코드북 + 공통 오버헤드 |
PQ에서 b=8이면 코드는 벡터당 m바이트를 차지합니다. b=4이면 지원되는 짝수 m에 대해 m/2바이트를 차지합니다. 예를 들어 벡터 1 million개에서 m=96, b=8이면 코드가 96000000바이트, row ID가 8000000바이트로, 합계는 104000000바이트(약 99.2 MiB)입니다. 압축률이 높은 인덱스를 추정할 때도 row ID를 빠뜨리면 안 됩니다.
| 추가 구성 요소 | 적용 대상 | 로드된 payload / 예산 산정 방법 |
|---|---|---|
| IVF 중심점 | 6개 유형 모두 | FP32: 유지되는 중심점 집합당 num_partitions * D * 4; 다른 구성에는 실제 요소 너비를 사용 |
| IVF 파티션 디렉터리 / 하위 인덱스 메타데이터 | 6개 유형 모두 | 유지되는 파티션 오프셋, 길이, 리더 구조를 측정; 벡터 값과 중심점과는 별개 |
| PQ 코드북 | IVF_PQ, IVF_HNSW_PQ | 표준 FP32 코드북: 유지되는 사본당 2^b * D * 4 |
| SQ 양자화 메타데이터 | IVF_SQ, IVF_HNSW_SQ | 유지되는 양자화 범위/파라미터를 측정; 버전마다 구성이 같다고 가정하지 말 것 |
| norm, 행 커버리지 구조, 추가 매핑 | 리더가 유지하는 경우 | 실제 배열/집합을 측정; 다중 벡터의 행 매핑에는 별도 예산이 필요 |
| 인덱스 객체, Arrow 유효성 버퍼, 여유 용량과 사본 | 6개 유형 모두 | 논리적 payload를 초과하는 할당을 측정; 공유 버퍼는 한 번만 계산 |
독립적인 인덱스 세그먼트는 각각 별도의 중심점 집합과 코드북을 유지할 수 있습니다. 공유 할당에 모든 파티션 수를 곱하거나 모든 사본이 공유된다고 가정하지 마세요. 추가 구조는 내장된 Lance 버전에 따라 달라집니다.
HNSW 그래프 구성 요소
그래프는 벡터 코드 및 row ID에 추가됩니다. UInt32 노드/이웃 ID와 선택적으로 유지되는 Float32 엣지 거리를 사용하는 Arrow 기반 Lance 그래프에서, E를 로드된 모든 레벨의 저장된 방향성 이웃 항목 수, V를 해당 레벨 전체에서의 노드 출현 횟수, A를 독립적으로 유지되는 그래프 배치 수라고 하겠습니다. 노드가 여러 레벨에 있으면 V에 여러 번 기여합니다.
| 그래프 구성 요소 | 주요 payload(바이트) | 계산 규칙 |
|---|---|---|
| 이웃 ID | 4 * E | 방향성 이웃 항목당 UInt32 하나; 모든 레벨을 계산 |
| 저장된 엣지 거리 | 4 * E | 이웃 항목당 Float32 하나; 리더가 거리 열을 건너뛰면 0 |
| 그래프 노드 ID | 4 * V | 노드 출현 건당 UInt32 하나; 데이터셋의 UInt64 row ID와는 별개 |
| 이웃 목록 오프셋 | 4 * (V + A) | 배치당 마지막 오프셋 하나를 포함하는 Int32 목록 오프셋 |
| 거리 목록 오프셋 | 4 * (V + A) | 두 번째 Int32 목록 오프셋 배열; 리더가 거리 열을 건너뛰면 0 |
| 거리를 포함한 소계 | 8 * E + 12 * V + 8 * A | 큰 배치에서는 약 8 * E + 12 * V; 전체 G는 아님 |
| 거리를 제외한 소계 | 4 * E + 8 * V + 4 * A | 노드 ID와 이웃 목록만 유지할 때 사용; 두 소계를 합하지 말고 하나를 선택 |
| 레벨 오프셋, 진입점, 노드 조회 구조 | 측정 | 로드된 그래프마다 유지되는 메타데이터와 조회 할당을 포함 |
| 유효성 버퍼, 그래프 객체, 여유 할당 용량 | 측정 | 위 버퍼를 초과하는 실제 할당을 더하되 공유 버퍼를 중복 계산하지 말 것 |
전체 그래프 예산 G | 주요 소계 + 측정한 그래프 추가 항목 | 벡터/row-ID payload와 다른 인덱스 구조에 더할 것 |
이는 구성 방식에 따른 추정치이며 노드당 바이트 수를 보장하는 고정값이 아닙니다. HNSW의 m은 그래프 연결성을 제어하며 PQ의 m과는 무관합니다. G 대신 N * m * 4를 사용하지 마세요. 실제 연결 차수, 상위 레벨, 유지되는 거리, 리더 버전이 모두 영향을 줍니다. 대표적인 파티션을 로드하고 유지되는 할당량을 측정해 추정치를 보정하세요.
예를 들어 캐시에 저장된 IVF_HNSW_PQ 파티션에 벡터 1 million개가 있고 PQ m=96, b=8, E=32000000, V=1100000, A=1이라고 가정하겠습니다. 이 리더가 거리 열을 유지한다고 가정합니다. 이 그래프 수치는 HNSW의 m에서 도출한 값이 아니라 설명을 위한 입력값입니다.
| 구성 요소 | 예시 payload |
|---|---|
| PQ 코드 | 96000000바이트 |
| 데이터셋 row ID | 8000000바이트 |
| 주요 그래프 버퍼 | 8 * 32000000 + 12 * 1100000 + 8 = 269200008바이트, 약 256.7 MiB |
| 코드 + row ID + 주요 그래프 버퍼 | 373200008바이트, 약 355.9 MiB |
| 대안: 거리를 건너뛸 때의 주요 그래프 버퍼 | 4 * 32000000 + 8 * 1100000 + 4 = 136800004바이트, 약 130.5 MiB |
| 대안 합계: 코드 + row ID + 거리를 제외한 그래프 버퍼 | 240800004바이트, 약 229.6 MiB |
| 어느 경우든 추가해야 할 항목 | 위 표의 그래프 추가 항목, IVF 중심점/디렉터리, PQ 코드북, 공통 오버헤드 |
리더가 유지하는 열에 맞는 예시를 선택하세요. 어느 소계도 완전한 캐시 용량 권장값은 아닙니다.
상위 프로젝트의 Lance 성능 가이드에서는 벡터 payload와 인덱스 구축 메모리 추정치를 설명합니다. Lance 벡터 인덱스 형식에서는 IVF 파티션, 하위 인덱스, 양자화된 스토리지의 구분을 설명합니다. 해당 문서의 최신 내용은 Doris에 내장된 Lance 버전보다 새로운 기능을 설명할 수 있으므로 호환성은 Lance Catalog 지원 매트릭스에서 확인하세요.
스칼라 및 전문 검색 인덱스 payload
스칼라 인덱스도 추정할 수 있지만 필요한 입력값은 인덱스 계열마다 다릅니다. 쿼리가 반환한 행만이 아니라 한 BE에 실제로 유지되는 항목을 계산하세요. 아래 방법은 Lance 스토리지 구성을 설명하며 Doris의 조건식 푸시다운 또는 인덱스 호환성 보장 범위를 넓히지 않습니다. 내장된 Lance 버전이 해당 인덱스를 지원하고 쿼리가 실제로 사용하는지 확인하세요.
BTree. 로드된 리프 페이지에는 값과 UInt64 row ID가 들어 있습니다. R을 캐시에 저장된 리프 페이지의 레코드 수, H를 해당 페이지 수, w를 고정 너비 값의 크기, L을 해당 페이지의 평균 문자열 길이(바이트)라고 하겠습니다.
| 리프 페이지 구성 요소 / 표현 방식 | 값 버퍼 | 오프셋 | UInt64 row ID |
|---|---|---|---|
| Int32 / Float32 | 4 * R | 없음 | 8 * R |
| Int64 / Float64 / 64-bit timestamp | 8 * R | 없음 | 8 * R |
| 기타 고정 너비 값 | w * R; 비트 단위로 패킹된 Boolean에는 실제 비트맵 버퍼를 사용 | 없음 | 8 * R |
| Arrow Utf8 / Binary | R * L | Int32 오프셋의 경우 4 * (R + H) | 8 * R |
| Arrow LargeUtf8 / LargeBinary | R * L | Int64 오프셋의 경우 8 * (R + H) | 8 * R |
리프 페이지 소계는 세 payload 열의 합계입니다. 열린 세그먼트마다 다음 구성 요소를 더하세요. 아래 P는 캐시에 저장된 페이지만이 아니라 해당 세그먼트의 모든 리프 페이지를 셉니다.
| 추가 BTree 구성 요소 | 주요 payload / 예산 산정 방법 |
|---|---|
| 조회용 최솟값 | 고정 너비 값은 P * w; 가변 너비 값은 실제 값/오프셋 버퍼 |
| 조회용 최댓값 | 고정 너비 값은 P * w; 가변 너비 값은 실제 값/오프셋 버퍼 |
| 조회용 NULL 개수 | UInt32 구성의 경우 4 * P |
| 조회용 페이지 번호 | UInt32 구성의 경우 4 * P |
| 고정 너비 조회 버퍼 소계 | P * (2 * w + 8); 아래 구조는 제외 |
| NULL 페이지 목록과 조회 객체 | 유지되는 목록, 객체, 구현별 사본을 측정 |
| 리프/조회 유효성 버퍼, Arrow 객체, 여유 용량 | 논리적 값/오프셋/row ID를 초과하는 할당을 측정 |
문자열 계산식은 표에 나온 Arrow 구성을 가정합니다. dictionary 또는 view 표현 방식에는 해당 방식의 실제 버퍼를 사용해야 합니다. 디스크 압축률로 디코딩된 페이지 크기를 결정할 수 없습니다. BTree의 기본 페이지 크기는 바이트가 아니라 레코드 4096개입니다. 인덱스의 실제 페이지 크기를 사용하세요. 조회 구조는 페이지를 설명한다는 이유만으로 BE 메타데이터 캐시 예산에 속하는 것이 아니라 인덱스 작업 집합에 속합니다.
예를 들어 Int64 값 100 million개의 리프 값/row-ID payload는 100000000 * 16 = 1600000000바이트(약 1.49 GiB)입니다. 캐시에 저장된 페이지가 레코드 10 million개를 포함한다면 해당 payload는 약 152.6 MiB입니다. 페이지당 레코드가 4096개라면 전체 인덱스에는 약 24415페이지가 있으며, 추가 구조를 제외한 조회 배치의 최솟값/최댓값/개수/페이지 번호 payload는 약 572 KiB입니다. 일치하는 행 열 개를 반환한다고 해서 레코드 열 개만 캐시에 저장한다는 뜻은 아닙니다. 리더는 페이지를 로드합니다.
Bitmap, LabelList 및 NGram. 아래에서 해당하는 구성 요소 행을 합산하세요. membership 비트맵의 비용이 일치하는 행당 자동으로 8바이트가 되는 것은 아닙니다. LabelList는 고유한 label별로 membership을 계산하며, 한 행이 여러 label 비트맵에 나타날 수 있습니다. NGram은 고유한 gram/행 membership을 계산하며, 한 행에 같은 gram이 반복되어도 membership이 늘어나지 않습니다. 이 NGram 인덱스는 n-gram tokenizer를 사용하는 FTS 인덱스와 다릅니다.
| 인덱스 계열 | 구성 요소 | 로드된 payload / 예산 산정 방법 |
|---|---|---|
| Bitmap | 키 조회 | 유지되는 키 값, 항목 참조, 조회 객체를 측정 |
| LabelList | label 조회 / wrapper | 고유한 label 키, 항목 참조, wrapper 객체를 측정 |
| NGram | gram 조회 | 유지되는 gram, 항목 참조, 조회 객체를 측정 |
| 세 계열 모두 | membership 비트맵: array container | 65536개 값의 범위에서 UInt16 항목 c개를 가진 일반적인 Roaring array container당 약 2 * c |
| 세 계열 모두 | membership 비트맵: dense container | 65536개 값의 범위를 다루는 일반적인 dense container당 8192바이트 |
| 세 계열 모두 | membership 비트맵: run / 전체 fragment 표현 방식 | 실제 표현 방식을 측정; 모든 대안을 합산하지 말고 container마다 선택된 표현 방식을 사용 |
| 세 계열 모두 | 비트맵 디렉터리 / 용량 | fragment/high-bit 디렉터리, container 객체, 여유 용량을 측정 |
| 존재하는 경우 | NULL / null-list 추적 | 유지되는 NULL membership 구조를 별도로 측정 |
| 각 계열의 합계 | 조회 구조 + 로드된 membership 표현 방식 + 디렉터리 + NULL 추적 | 공유 할당은 한 번만 계산; 직렬화된 비트맵 크기가 상주 메모리 크기를 보장하지는 않음 |
예를 들어 로드된 dense container가 256개이면 키, 디렉터리, NULL 추적, 객체 오버헤드를 제외하고도 bitset payload가 2 MiB입니다.
기타 스칼라 및 전문 검색 인덱스. 각 행은 별도의 구성 요소를 나타냅니다. 소계 행은 앞선 구성 요소의 합계를 보여 주므로 다시 더하면 안 됩니다. BE에 유지되는 콘텐츠만 계산하세요. 측정은 리더의 실제 할당량이 필요하다는 뜻이며, 해당 구성 요소를 생략해도 된다는 뜻이 아닙니다.
| 인덱스 계열 | 구성 요소 | 로드된 payload / 예산 산정 방법 |
|---|---|---|
| FTS / Inverted | 검색어 사전 | 로드된 검색어, 사전 구조, 조회 할당을 측정 |
| FTS / Inverted | 문서 메타데이터 | 유지되는 문서 매핑, 길이, 기타 리더 메타데이터를 측정 |
| FTS / Inverted | 비압축 posting row ID | UInt64 검색어/문서 항목 T개에 대해 8 * T |
| FTS / Inverted | 비압축 posting 빈도 | Float32 빈도에 대해 4 * T; ID와 빈도의 합계는 12 * T |
| FTS / Inverted | 활성화된 경우의 위치 정보 | 저장된 Int32 출현 항목 O개에 대해 4 * O |
| FTS / Inverted | 위치 오프셋 | 실제로 유지되는 오프셋 배열의 바이트 수; 4 * O와는 별개 |
| FTS / Inverted | 압축된 posting 구성 | 해당하는 비압축 구성 계산식 대신 실제로 유지되는 압축 블록을 사용 |
| FTS / Inverted | scoring / 블록 메타데이터와 할당 오버헤드 | 유지되는 블록 설명자, scoring 데이터, 객체, 여유 용량을 측정 |
| ZoneMap | 최솟값 | 로드된 zone Z개와 너비 w인 고정 너비 값의 경우 논리적으로 Z * w바이트 |
| ZoneMap | 최댓값 | 논리적으로 Z * w바이트; 최솟값과 최댓값의 합계는 2 * Z * w |
| ZoneMap | 개수와 zone 경계 | 유지되는 개수와 경계를 측정; 부분 zone을 포함해 Fragment별 zone을 계산 |
| ZoneMap | NULL 추적과 boxed-value/객체 오버헤드 | 선택적인 NULL 행 집합과 실제 스칼라 할당을 측정; 논리적인 최솟값/최댓값의 바이트 수보다 클 수 있음 |
| BloomFilter | 필터 비트 배열 | 실제 할당된 필터 바이트 수를 합산; 크기가 각각 F바이트로 같은 필터 Z개의 경우 Z * F |
| BloomFilter | zone 설명자 / 필터 객체 | 유지되는 설명자, 필터 파라미터, 객체 오버헤드를 측정 |
| BloomFilter | NULL 추적 | 선택적인 NULL 행 집합을 측정 |
| RTree | bounding-box 좌표 | 2D 항목당 Float64 좌표 네 개에 대해 32 * Q; 캐시에 저장된 모든 트리 레벨의 항목 Q개를 계산 |
| RTree | 행 / 하위 페이지 ID | UInt64 ID에 대해 8 * Q; 좌표와 ID의 합계는 40 * Q |
| RTree | 트리 메타데이터와 페이지 오프셋 | 유지되는 트리 설명자와 페이지 오프셋 버퍼를 측정 |
| RTree | NULL 추적, Arrow/객체, 여유 용량 | 실제 추가 할당을 측정 |
| JSON-path wrapper | 기반 인덱스 | 선택한 스칼라 인덱스 계열의 전체 구성 요소 예산을 사용 |
| JSON-path wrapper | 경로와 wrapper 구조 | 기반 인덱스 외에 유지되는 경로와 wrapper 할당을 측정 |
Bloom filter에는 구축 도구가 실제로 블록 단위로 반올림해 할당한 크기가 필요합니다. 행 수와 false-positive 확률만으로는 모든 구성을 설명할 수 없습니다. 익숙하지 않거나 실험적인 구성에는 다른 계열의 계산식을 적용하지 말고 로드된 할당량을 측정하세요. 일치하는 행이 적더라도 사전, 트리 또는 문서 메타데이터에 상당한 메모리가 필요할 수 있습니다.
쿼리 작업 메모리는 모든 캐시 표와 별도로 계산하세요. 임시 교집합/합집합, 결과 row-ID 집합, 근사 필터의 재검사, 정확한 RTree 검사를 위한 원본 geometry 읽기, 출력 열 읽기에는 각각 예산이 필요합니다.
기반 구조에 관해서는 상위 프로젝트의 BTree 형식, Bitmap 형식, FTS 형식을 참조하세요. 위 계산식은 압축된 파일 크기나 전체 쿼리 RSS가 아니라 로드된 payload를 추정합니다.
인덱스 크기에서 BE별 작업 집합 산출
세그먼트 s에 대해 N_s를 인덱싱된 벡터 수, P_s를 IVF 파티션 수, H_s를 특정 BE에 유지할 서로 다른 파티션 수라고 하겠습니다. 파티션이 대체로 균형 잡혀 있다면 다음과 같습니다.
cached_vectors_on_BE ≈ sum over segments (N_s * H_s / P_s)
index_cache_target ≈ cached vector/row-ID payload
+ cached HNSW graphs
+ centroids, codebooks and other index allocations
+ measured headroom
파티션 크기가 불균형하면 실제 파티션 행 수를 사용하세요. H_s는 한 쿼리의 nprobes만이 아니라 여러 쿼리에 걸친 작업 집합을 나타냅니다. 다양한 쿼리가 결국 모든 파티션에 접근할 수 있고, 반복되거나 동시 실행되는 쿼리는 같은 캐시 파티션을 공유할 수 있습니다. 서로 다른 (dataset, index segment, partition) 항목을 세고, 해당 BE가 접근하는 모든 테이블, 인덱스, 버전을 포함하세요. 한 BE의 캐시 상주 콘텐츠가 다른 BE의 캐시를 워밍업하지는 않으며, 스케줄링이 모든 인덱스 세그먼트를 한 BE에 유지한다고 보장하지도 않습니다. 관측된 배치와 재사용 양상이 뒷받침하지 않는다면 전체 인덱스 크기를 BE 수로 나누지 마세요.
쿼리 실행 중 메모리는 별도로 필요합니다. 진행 중인 파티션 로드, 캐시 퇴출 후에도 쿼리가 참조하는 객체, Prefilter 상태, 거리 테이블, 후보 힙, 디코딩된 배치, refinement, 두 번째 단계의 데이터 가져오기를 포함합니다. top_k, ef, refine_factor, 스캔 동시 실행 수준, nprobes는 이 메모리나 접근하는 작업 집합에 영향을 주지만 벡터당 저장 바이트 수를 바꾸지는 않습니다. 구성한 캐시 용량은 Lance 또는 BE RSS의 엄격한 상한이 아닙니다. 캐시를 늘려도 거리 계산이나 쿼리별 필터링 작업이 없어지지 않습니다.
구성 예시와 튜닝
벡터가 100 million개이고 차원이 768인 FP32 IVF_PQ 인덱스가 있다고 가정하겠습니다. m=96, b=8이며, 인덱스 세그먼트 하나에 균형 잡힌 파티션 4096개가 있습니다. 한 BE의 대표적인 워크로드가 서로 다른 파티션 1024개에 반복적으로 접근합니다.
Full code/row-ID payload = 100000000 * (96 + 8) = 10400000000 bytes ≈ 9.69 GiB
Cached payload = 10400000000 * 1024 / 4096 = 2600000000 bytes ≈ 2.42 GiB
IVF centroids = 4096 * 768 * 4 = 12582912 bytes = 12 MiB
PQ codebook = 256 * 768 * 4 = 786432 bytes = 0.75 MiB
이것이 해당 BE의 유일하게 큰 인덱스 작업 집합이고 메모리 예산이 허용한다면, 계산된 payload에 추가 인덱스 할당을 위한 여유를 둔 4 GiB가 이 예시의 합리적인 초기 인덱스 캐시 할당량입니다. 이는 검증할 초기값이지 보편적인 오버헤드 비율이 아닙니다. 워크로드가 전체 인덱스를 상주시켜야 한다면 4 GiB는 부족하며, 기본값인 10 GiB조차 다른 할당을 제외한 9.69 GiB의 payload에 근접합니다.
4 GiB의 인덱스 캐시와 1 GiB의 메타데이터 캐시를 최대 쿼리 메모리 사용량, 다른 Doris 워크로드, 프로세스/OS 여유 메모리와 함께 수용할 수 있는 BE에서는 다음과 같이 설정하세요.
lance_index_cache_size_bytes = 4294967296
lance_metadata_cache_size_bytes = 1073741824
설정을 적용하려면 해당 BE를 재시작하세요. 예산은 BE마다 따로 적용하세요. 이는 BE 구성 파라미터이며 Catalog 속성이나 SQL 세션 변수가 아닙니다.
- 서로 다른 쿼리 벡터, 필터, 테이블을 포함한 대표적인 쿼리와 동시 실행 수준으로 워밍업하세요. 벡터 하나만 반복하면 다양한 워크로드의 작업 집합을 과소평가하게 됩니다.
doris_be_lance_session_index_cache_usage_bytes와doris_be_lance_session_index_cache_capacity_bytes를 비교하고, 구간별hits_total/misses_total증가량을 관찰하세요. 완료된 쿼리마다LanceIndex아래의LanceIndexCacheHits,LanceIndexCacheMisses,LanceIndexObjectLoads,LanceIndexComponentsLoaded도 확인하세요. 쿼리 캐시 조회와 구성 요소 로드는 범위가 다르므로 BE 전체 메트릭의 변화량과 서로 바꿔 해석할 수 없습니다. 사용량이 용량에 가까운 상태에서 miss가 지속되면 캐시 항목 교체가 잦다는 신호일 수 있습니다. 하지만 새 파티션, 인덱스 버전, 서로 다른 BE, 콘텐츠를 유지하지 않는 스트리밍 경로도 miss를 일으킬 수 있습니다. 재사용 양상과 가용 메모리가 뒷받침할 때만 용량을 늘리세요.- BE 메모리 예산이 허용하면 기본값인 1 GiB에서 메타데이터 캐시 크기 조정을 시작하세요.
usage_bytes와 hit/miss 메트릭을 보고 조정하세요. File/Fragment 수, schema/페이지 메타데이터, 삭제 정보, row-ID 매핑이 수요를 결정합니다. 벡터 인덱스 크기의 고정된 비율로 정하지 마세요. BE 메타데이터 캐시는 FE 테이블 접근 캐시와 별개입니다. - 두 캐시 중 하나를 늘리기 전에 목표 동시 실행 수준에서 BE 프로세스 메모리와 쿼리 최대 사용량을 확인하세요. 캐시 적중률이 높아도 지연 시간이 길다면 scoring, Prefilter, 디코딩 또는 데이터 가져오기 비용이 원인일 수 있습니다. 이때 캐시를 추가해도 반드시 도움이 되는 것은 아닙니다.
- refinement와 출력 열 가져오기처럼 반복되는 데이터 파일 읽기에 맞춰
lance_data_cache_disk_capacity_bytes의 크기를 독립적으로 정하세요. 이를 늘려도 인덱스 캐시가 커지거나 인덱스 캐시 miss가 보완되지는 않습니다. 인덱스 파일은 이 디스크 데이터 캐시를 우회합니다. 처음에는lance_data_cache_read_block_size_bytes를 기본값인 1 MiB로 유지하고, 유효 읽기와 읽기 증폭을 측정한 뒤에만 변경하세요.
4. 공유 캐시 예산 산정
각 BE에서 Lance는 공유 세션과 하나의 인덱스 캐시 용량을 사용합니다. 벡터 파티션, BTree 페이지, Bitmap/LabelList 항목, 전문 검색 인덱스 콘텐츠는 같은 BE에서 접근하는 다른 테이블 및 Catalog의 인덱스를 포함해 이 공간을 놓고 경쟁할 수 있습니다. 캐시 키는 각 항목의 ID를 구분하지만 용량을 예약하지는 않습니다. Lance BE 캐시 설정에는 테이블별 또는 인덱스 유형별 할당량이나 고정 유지 제어 기능이 없습니다.
로드된 콘텐츠만 캐시 공간을 사용합니다. 인덱스를 여러 개 만들었다는 이유만으로 모두 로드되지는 않습니다. 쿼리가 인덱스를 사용하기 시작하면 한 인덱스를 로드하면서 다른 인덱스의 항목이 퇴출될 수 있습니다. 예를 들어 광범위한 BTree 범위 스캔이나 다양한 FTS 쿼리가 워밍업된 벡터 파티션을 대체할 수 있습니다. 이후의 벡터 쿼리는 해당 파티션을 다시 로드합니다. 인덱스 버전이 바뀌면 이전 항목이 상주하는 동안 새 항목이 추가될 수도 있습니다. 실행 중인 쿼리가 여전히 참조하는 객체는 퇴출되어도 즉시 해제되지 않습니다.
공유 할당을 중복 계산하지 않고 혼합 작업 집합을 계획하세요.
index_cache_target_on_BE ≈ vector working set
+ BTree working set
+ Bitmap / LabelList / NGram working sets
+ FTS and other index working sets
+ measured headroom
예를 들어 측정값과 payload 추정치가 인덱스 구조를 포함해 벡터에 2.5 GiB, BTree에 0.25 GiB, Bitmap에 0.5 GiB, FTS에 0.75 GiB라고 가정하겠습니다. 합산 작업 집합은 4 GiB입니다. 초기 인덱스 캐시를 5 GiB로 설정하면 증가분과 추정 오차를 위한 1 GiB가 남지만 모든 워크로드가 들어간다고 보장하지는 않습니다. BE에 1 GiB의 메타데이터 캐시와 함께 최대 쿼리 메모리 사용량, 다른 Doris 워크로드, 프로세스/OS 여유 메모리를 수용할 공간도 있다면 다음과 같이 설정하세요.
lance_index_cache_size_bytes = 5368709120
lance_metadata_cache_size_bytes = 1073741824
인덱스 캐시와 메타데이터 캐시는 용량이 별개입니다. 메타데이터 항목이 인덱스 항목을 직접 퇴출시키지는 않으며, 한 캐시를 늘려도 다른 캐시가 커지지 않습니다. 다만 둘 다 같은 BE 프로세스 메모리를 사용합니다. 디스크 데이터 캐시는 세 번째 예산 항목이며 인덱스 파일을 캐시에 저장하지 않습니다. 같은 BE의 서로 다른 Catalog도 인덱스 캐시를 격리하지 않습니다. 워크로드 격리가 필요하다면 별도의 BE 리소스와 배포 환경에서 지원하는 라우팅이 필요합니다.
경합을 진단하려면 먼저 한 쿼리 계열을 워밍업하고 지연 시간과 캐시 메트릭 변화량을 기록하세요. 두 번째 계열을 실행한 뒤 첫 번째 계열을 다시 실행하세요. 인덱스 버전과 BE 배치가 비슷하게 유지되는 조건에서 캐시 사용량이 용량에 가까워지고 miss나 재로드가 다시 늘어난다면 퇴출 압박을 시사합니다. 세션 hit/miss 메트릭은 워크로드를 합산하므로 쿼리 수준의 LanceIndexCacheHits/LanceIndexCacheMisses, LanceIndexComponentsLoaded, LanceIOReadBytes와 함께 해석하세요. 구성 요소 로드 수는 miss 수가 아니며 실행 시 읽은 바이트 수는 측정된 원격 트래픽이 아닙니다. 모든 인덱스를 미리 워밍업하면 유지하려던 작업 집합이 오히려 퇴출될 수 있습니다. 대표적인 혼합 워크로드로 워밍업하고, 재사용 양상과 가용 메모리가 뒷받침할 때만 용량을 늘리세요.
5. 인덱스 세그먼트 단위 선택
Index Segment, 데이터 Fragment, IVF 파티션, BTree 리프 페이지는 서로 다른 단위입니다. 현재 Doris 벡터 검색 경로에서는 선택된 물리적 Index Segment마다 스캔 Split 하나가 생성되고, 인덱스로 커버되지 않는 Fragment는 Flat Search Split을 추가합니다. FTS도 물리적 인덱스 세그먼트를 사용합니다. 벡터 세그먼트 하나의 IVF 파티션은 BE들에 분산되는 별도의 Doris Split이 아닙니다. Lance 내부에서는 여전히 작업을 병렬로 실행할 수 있습니다. 일반적인 스칼라 스캔에서 세그먼트 기반 그룹화는 선택된 인덱스와 실행 계획에 따라 달라지므로, 모든 스칼라 인덱스가 벡터 검색의 Split 정책을 따른다고 가정하지 마세요.
B를 실제로 해당 쿼리를 실행할 수 있는 BE 수, S를 선택된 벡터 세그먼트 수라고 하겠습니다. S가 B보다 작으면 해당 쿼리에서 그 BE들을 모두 활용하기에는 인덱스 스캔 Split 수가 부족합니다. S를 늘리면 스케줄링 기회가 늘지만 모든 Split이 동시에 실행된다고 보장하지는 않습니다. 스캐너 제한, Lance 내부 동시 실행 수준, CPU, I/O, 캐시 상주 상태, 동시 실행 쿼리도 영향을 줍니다. Doris 파이프라인 인스턴스만 늘려서는 인덱스 세그먼트 하나를 세분할 수 없습니다.
대규모 데이터셋에서는 균형이 대체로 맞는 벡터 세그먼트 B, 2 * B, 4 * B개를 초기 실험으로 비교하세요. 이는 공식적인 최적값이나 소규모 데이터셋의 필수 요건이 아닙니다. 벡터가 100 million개이고 적격 BE가 8개인 예시는 다음과 같습니다.
| 세그먼트 수 | 세그먼트당 평균 벡터 수 | 평가할 항목 |
|---|---|---|
| 8 | 12.5 million | 세그먼트별 작업 수를 줄이면서 BE를 모두 활용할 만큼 Split이 충분한지 |
| 16 | 6.25 million | 스케줄링 유연성이 높아지고 큰 세그먼트 하나의 영향을 덜 받는지 |
| 32 | 3.125 million | 작업 수 증가와 함께 검색, 로드, 병합 오버헤드가 얼마나 커지는지 |
Fragment 수만이 아니라 예상 검색 작업량과 로드되는 인덱스 바이트 수의 균형을 맞추세요. 차원과 인코딩이 같은 벡터에서는 행 수를 같게 하는 것이 유용한 출발점이지만, HNSW 그래프 크기, IVF 불균형, 스칼라 선택도, 문서 길이에 따라 균형이 달라질 수 있습니다. 모든 상황에 맞는 세그먼트당 행 수나 파일 크기 임계값은 없습니다. 처리량이 중요한 워크로드에서는 동시 실행 쿼리를 위한 여력을 유지하도록 쿼리당 작업 수가 적은 편이 유리할 수 있고, 단일 쿼리의 지연 시간에는 Split 수가 많은 편이 도움이 될 수 있습니다.
세그먼트가 늘면 세그먼트별 준비 작업, 메타데이터/양자화 객체, 로컬 후보 생성도 늘어납니다. 각 벡터/FTS Split은 최대 top_k + offset개의 후보를 유지하며 Doris는 이 상한을 S로 나누지 않습니다. 따라서 스캔 측 후보 수의 상한은 일치 항목 수가 충분할 때 S * (top_k + offset)입니다. top_k=1000, offset이 없고 Split이 32개이면 Doris가 후보를 줄이기 전에 최대 32000개입니다. 로컬 TopN이 네트워크 트래픽을 줄일 수 있으므로 이것이 네트워크로 전송되는 행 수를 보장하지는 않습니다. top_k가 클수록 후보 처리 비용이 특히 중요합니다.
세그먼트 수는 각 세그먼트의 IVF num_partitions 및 쿼리 nprobes와 함께 튜닝하세요. 파티션이 균형 잡혀 있다면 탐색하는 벡터 수는 대략 sum(N_s * nprobes_s / P_s)이며, 세그먼트마다 N_s를 상한으로 합니다. 이는 HNSW 비교 횟수 계산식이 아닙니다. 세그먼트를 재구축하거나 파티션 수를 바꾸면 같은 nprobes에서도 recall이 달라질 수 있습니다. 비슷한 recall을 기준으로 지연 시간을 비교하세요. HNSW ef와 refinement 설정도 비교 결과에 영향을 줍니다.
Lance 도구로 세그먼트를 구축하고 유지 관리한 뒤, 의도한 논리적 인덱스로 commit하세요. 전체 테이블을 대상으로 각각 별도 이름을 가진 인덱스 여러 개는 물리적 세그먼트가 여러 개인 인덱스 하나를 대체하지 못합니다. 완전한 Fragment 단위로 균형 잡힌 그룹을 구성하고, 작은 append로 인해 미세한 세그먼트가 많이 쌓이지 않도록 하며, 내장된 Lance 버전에서 지원하는 유지 관리 API를 사용하세요. Doris EXPLAIN/Profile에서 결과로 생성된 물리적 세그먼트 수와 인덱스로 커버되지 않는 Fragment 수를 확인하세요. SHOW INDEX의 행 수로 추론하지 마세요. 모든 세그먼트를 하나로 병합하면 Doris 스캔 병렬성이 줄어들 수 있습니다. 세그먼트 구축과 commit 개념은 Lance 분산 인덱싱을 참조하세요.
BTree 지점 조회나 선택도가 높은 Bitmap 쿼리에서는 세그먼트를 많이 나눠도 세그먼트별로 줄어드는 작업이 적은 반면 조회와 열기 작업이 늘 수 있습니다. FTS에는 세그먼트별 사전/posting 검색과 후보 병합이 추가됩니다. 모든 인덱스 계열에 벡터 세그먼트 수를 적용하지 말고 워크로드별로 평가하세요. 세그먼트를 나누는 것만으로 불필요한 행별 Prefilter 구성이 없어지지도 않습니다. 리더의 필터링 동작은 별도로 확인하세요.
6. 혼합 워크로드 검증
- 인덱스 선택, 물리적 Split, 인덱스 커버리지를 확인하세요. 대체 스캔과 두 번째 단계의 데이터 가져오기도 지연 시간 분석에 포함하세요.
- 각 인덱스의 유지되는 payload를 추정하고 공유 작업 집합을 합산하세요. 병렬 로드, row-ID 집합, 후보 힙, 디코딩을 포함한 쿼리 메모리는 별도로 확보하세요.
- 실제와 비슷한 동시 실행 수준에서 cold cache와 워밍업된 혼합 워크로드를 비교하세요. P95 지연 시간, 처리량, 해당하는 경우 recall, BE별 최대 메모리 사용량, 캐시 사용량과 hit/miss 변화량, 인덱스 로드 I/O, 스캔 시간 불균형을 추적하세요.
- 캐시 용량, 세그먼트 수, 검색 파라미터 중 한 번에 한 요소만 변경하세요. 세그먼트 재구축으로 검색 모델이 바뀌었다면 recall을 다시 확인하세요. 처리량, 지연 시간, 메모리 사용량이 더 이상 개선되지 않으면 Split이나 캐시를 늘리지 마세요.
- 상당한 양의 append, 인덱스 재구축, BE 규모 조정, 쿼리 구성 변경 후에는 검증을 반복하세요. 벡터나 테이블 하나에 맞춰 조정한 구성은 모든 워크로드에 안정적으로 적용할 수 있는 예산이 아닙니다.
7. 리소스를 늘리기 전에 진단하기
| 관찰된 현상 | 먼저 확인할 항목 | 평가할 조치 |
|---|---|---|
| 캐시 사용량이 용량에 가까운 상태에서 인덱스 miss가 반복됨 | 인덱스/버전 변경, BE 배치, 합산 작업 집합 | 메모리가 허용하면 lance_index_cache_size_bytes를 늘리거나, 불필요한 워밍업을 줄이거나, 별도의 BE 리소스로 워크로드를 격리 |
| 스칼라 또는 FTS 쿼리 후 벡터 쿼리가 느려짐 | 공유 캐시 예산 산정의 워크로드 간 퇴출 점검을 반복 | 두 쿼리 계열을 함께 고려해 예산을 잡을 것; 같은 BE의 별도 Catalog는 캐시를 격리하지 않음 |
| 대규모 인덱스 데이터셋을 스캔하는 BE가 적음 | 물리적 세그먼트 Split 수, 적격 BE, 스캐너 스케줄링 | 균형 잡힌 세그먼트 수를 늘려 비교; 파이프라인 인스턴스만 늘려서는 세그먼트를 분할할 수 없음 |
| 세그먼트 수를 늘린 뒤 CPU, 메모리 또는 병합 시간이 증가함 | Split당 후보 수, top_k + offset, 쿼리 동시 실행 수준 | 세그먼트 수를 줄이거나 애플리케이션에 필요한 수보다 많은 후보를 요청하지 말 것; 검색 설정을 바꾸면 recall을 재확인 |
| 캐시 hit는 많지만 지연 시간이 여전히 김 | scoring, Prefilter 작업, 디코딩, 두 번째 단계의 데이터 가져오기 시간 | 지배적인 단계를 최적화; 인덱스 캐시를 늘려도 계산이나 데이터 가져오기는 없어지지 않음 |
| 메타데이터 재로드가 계속됨 | 메타데이터 캐시 사용량과 hit/miss 변화량, Fragment/파일 수와 버전 교체 | 재사용 양상과 메모리가 허용하면 lance_metadata_cache_size_bytes를 독립적으로 조정 |
| 캐시 사용량이 용량보다 낮은데 프로세스 메모리는 높음 | 동시 실행 쿼리의 할당, 활성 로드, 유지되는 객체, 다른 BE 워크로드 | 측정된 최대 사용량에 따라 동시 실행 수준이나 캐시 예산을 낮출 것; 캐시 용량은 프로세스 메모리의 상한이 아님 |
| 데이터 파일 I/O가 계속 높음 | 디스크 캐시 카운터, 재사용 양상, 읽기 블록 크기, 쿼리 출력 열 | 데이터 캐시 예산이나 읽기 단위를 조정; 인덱스 파일은 이 디스크 데이터 캐시를 사용하지 않음 |
Query Profile 참고 문서를 사용해 FE 메타데이터/계획 수립, BE 인덱스/스캔 작업, 두 번째 단계의 row-ID 데이터 가져오기를 구분하세요. 캐시 카운터와 BE 전체 메트릭은 캐시 효율 확인을 참조하세요. 합산 적중률만으로는 어떤 인덱스가 퇴출되었는지 또는 어느 단계가 지연 시간을 지배하는지 알 수 없습니다. 한 번에 설정 하나만 바꾸고, 각 비교의 워크로드, 인덱스 버전, BE 배치, 지연 시간, 처리량, recall, 최대 메모리 사용량을 기록하세요.