Qdrant에서 멀티테넌시와 커스텀 샤딩 구현하기

Qdrant에서 멀티테넌시와 커스텀 샤딩 구현하기 (How to Implement Multitenancy and Custom Sharding in Qdrant)

머신러닝 설정 확장하기: Qdrant에서 멀티테넌시와 커스텀 샤딩의 힘

멀티테넌시분산 배포 주제가 우리 Discord 지원 채널에 매일 올라오고 있어요. 그만큼 많은 분들이 머신러닝 설정의 나머지와 함께 Qdrant를 확장하려고 한다는 뜻이죠.

은행 사기 탐지 시스템을 만들든, 전자상거래용 RAG를 만들든, 연방 정부용 서비스를 만들든, 제품을 확장하려면 멀티테넌트 아키텍처를 활용해야 해요. SaaS와 엔터프라이즈 앱의 세계에서 이 설정은 표준이에요. 애플리케이션의 성능을 크게 높이고 호스팅 비용을 낮춰 주죠.

출처: 공식문서

Qdrant의 멀티테넌시 & 커스텀 샤딩

우리는 바로 이것을 위해 두 가지 주요 기능을 개발했어요. 이제 단일 Qdrant 클러스터를 확장해서 전 세계의 모든 고객을 지원할 수 있어요. 멀티테넌시 아래에서 각 고객의 데이터는 완전히 격리되고 고객만이 접근할 수 있어요. 때로는 이 데이터가 위치에 민감한 경우, Qdrant는 클러스터를 지역이나 다른 기준으로 나눠 고객의 접근을 더 안전하게 만들 옵션도 제공해요. 이것을 커스텀 샤딩이라고 불러요.

이 둘을 결합하면 단일 Qdrant 클러스터의 편리함을 더 잘 활용하는 효율적으로 분할된 아키텍처가 만들어져요. 이 글에서는 그 이점을 간단히 설명하고 두 기능을 시작하는 방법을 보여 드릴게요.

하나의 컬렉션, 많은 테넌트

Qdrant에서 작업할 때 모든 데이터를 단일 컬렉션에 upsert한 다음, 각 벡터를 payload를 통해 분할할 수 있어요. 즉 모든 사용자가 단일 Qdrant 클러스터의 힘을 활용하면서도, 그들의 데이터는 컬렉션 안에서 여전히 격리돼 있어요. 두 테넌트 컬렉션을 살펴볼게요.

그림 1: 각 개별 벡터에 어떤 테넌트에 속하는지 나타내는 특정 payload가 할당된다. 이것이 많은 수의 서로 다른 테넌트가 단일 Qdrant 컬렉션을 공유할 수 있는 방식이다.

Qdrant Multitenancy

Qdrant는 수많은 테넌트가 있는 단일 컬렉션에서 뛰어난 성능을 내도록 설계됐어요. 데이터가 동질적이지 않거나 사용자의 벡터가 서로 다른 임베딩 모델로 만들어질 때만 여러 컬렉션을 만들어야 해요. 컬렉션을 너무 많이 만들면 리소스 오버헤드와 의존성이 생길 수 있어요. 이는 비용을 높이고 전반적인 성능에 영향을 줄 수 있죠.

데이터베이스 샤딩

Qdrant에서 각 벡터에 대해 샤드를 개별적으로 지정할 수도 있어요. 이 기능은 클러스터에서 데이터가 어디에 보관되는지 제어하고 싶을 때 유용해요. 예를 들어 벡터 한 세트는 자체 노드의 한 샤드에, 다른 세트는 완전히 다른 노드에 할당할 수 있어요.

벡터 검색 동안 여러분의 연산은 실제로 필요한 샤드의 부분집합만 건드리게 돼요. 대규모 배포에서는 컬렉션 전체를 스캔할 필요가 없는 연산의 성능을 크게 개선할 수 있어요.

이것은 반대 방향으로도 동작해요. 뭔가를 검색할 때마다 샤드 하나 또는 여러 개를 지정할 수 있고, Qdrant는 그것들이 어디 있는지 알게 돼요. 클러스터의 모든 머신에 결과를 묻는 것을 피하죠. 이는 오버헤드를 최소화하고 성능을 극대화해요.

일반적인 사용 사례

이 기능의 명확한 사용 사례는 멀티테넌트 컬렉션을 관리하는 거예요. 각 테넌트(사용자든 조직이든)가 분리되어 있다고 가정하므로, 그들의 데이터를 별도의 샤드에 저장할 수 있죠. 샤딩은 특정 데이터를 특정 위치에 보관해야 하는 지역 기반 데이터 배치 문제도 해결해요. 다만 그러려면 샤드를 노드 사이에서 이동하는 것이 필요해요.

그림 2: 사용자는 모두 같은 컬렉션 안에서 자신과 관련된 샤드를 upsert하고 쿼리할 수 있다. 지역 샤딩은 대륙 간 트래픽을 피하는 데 도움이 될 수 있다.

Qdrant Multitenancy

커스텀 샤딩은 다른 사용 사례에 대한 정밀한 제어도 제공해요. 시간 기반 데이터 배치는 데이터 스트림이 최신 업데이트를 나타내는 샤드를 인덱스하게 해요. 샤드를 날짜별로 구성하면 검색된 데이터의 최신성을 크게 제어할 수 있어요. 시간에 민감한 데이터에 크게 의존하는 소셜 미디어 플랫폼에 특히 관련 있죠.

더 나아가기 전에.....사용자 데이터는 얼마나 안전한가요?

설계상 Qdrant는 세 가지 수준의 격리를 제공해요. 처음에는 컬렉션 기반 격리를 도입했지만, 확장된 설정에서는 이 수준을 넘어서야 해요. 이 시나리오에서는 (멀티테넌시에서 오는) payload 기반 격리와 (샤딩에서 오는) 리소스 기반 격리를 활용하게 돼요. 궁극적인 목표는 단일 컬렉션을 두고 그 안에서 샤드 배치를 더 정밀하게 조작·커스터마이즈하여 어떤 오버헤드도 피하는 거예요. 아래 다이어그램은 2계층 격리 배치 안에서 데이터가 어떻게 배열되는지 보여 줘요.

그림 3: 사용자는 group_id와 개별 shard_key_selector 두 가지 필터로 컬렉션을 쿼리할 수 있다. 이는 데이터에 두 가지 추가 격리 수준을 제공한다.

Qdrant Multitenancy

단일 컬렉션을 위한 커스텀 샤드 만들기

컬렉션을 만들 때 사용자 정의 샤딩을 구성해야 해요. 이를 통해 데이터의 샤드 배치를 제어해서, 연산이 실제로 필요한 샤드의 부분집합만 건드리게 할 수 있어요. 큰 클러스터에서는 데이터를 가져오기 위해 컬렉션 전체를 거칠 필요가 없어서 연산 성능이 크게 개선돼요.

client.create_collection(
    collection_name="{tenant_data}",
    # number of physical shards per shard key, not the number of shard keys
    shard_number=1,
    sharding_method=models.ShardingMethod.CUSTOM,
    # ... other collection parameters
)
client.create_shard_key("{tenant_data}", "canada")
client.create_shard_key("{tenant_data}", "germany")

이 예제에서 클러스터는 독일과 캐나다로 나뉘어요. 캐나다와 독일의 법은 국제 데이터 전송에 관해 다르죠. 헬스케어 산업을 지원하는 RAG 애플리케이션을 만든다고 해 볼게요. 규정 준수를 위해 캐나다 고객 데이터는 독일 고객 데이터와 명확히 분리되어야 해요.

같은 컬렉션의 일부여도 각 샤드의 데이터는 다른 샤드와 격리되고 그렇게 검색될 수 있어요. 샤드와 검색에 대한 추가 예제는 Distributed Deployments 문서와 Qdrant Client 사양을 참고하세요.

사용자용 멀티테넌트 설정 구성하기

이제 데이터 추가를 시작해 볼게요. 새 컬렉션에 벡터를 upsert할 때 각 벡터에 group_id 필드를 추가할 수 있어요. 이렇게 하면 Qdrant가 각 벡터를 해당 그룹에 할당해요.

또한 각 벡터는 이제 샤드에 할당될 수 있어요. 각 개별 벡터에 shard_key_selector를 지정할 수 있죠. 이 예제에서는 tenant_1에 속하는 데이터를 캐나다 지역에 upsert하고 있어요.

client.upsert(
    collection_name="{tenant_data}",
    points=[
        models.PointStruct(
            id=1,
            payload={"group_id": "tenant_1"},
            vector=[0.9, 0.1, 0.1], 
        ),
        models.PointStruct(
            id=2,
            payload={"group_id": "tenant_1"},
            vector=[0.1, 0.9, 0.1],
        ),
    ],
    shard_key_selector="canada",
)

group_id의 데이터는 격리된다는 점을 기억하세요. 아래 예제에서 tenant_1 벡터는 tenant_2와 분리되어 있어요. 첫 번째 테넌트는 클러스터의 캐나다 부분에서 자신의 데이터에 접근할 수 있어요. 반면 아래에서 볼 수 있듯 tenant_2는 독일에 호스팅된 정보만 검색할 수 있을 거예요.

client.upsert(
    collection_name="{tenant_data}",
    points=[
        models.PointStruct(
            id=3,
            payload={"group_id": "tenant_2"},
            vector=[0.1, 0.1, 0.9],
        ),
    ],
    shard_key_selector="germany",
)

필터로 데이터 검색하기

데이터 검색 기준을 지정하면서 접근 제어 설정이 완성돼요. 벡터를 검색할 때는 query_filtergroup_id와 함께 사용해 각 사용자의 벡터를 필터링해야 해요.

client.query_points(
    collection_name="{tenant_data}",
    query_filter=models.Filter(
        must=[
            models.FieldCondition(
                key="group_id",
                match=models.MatchValue(
                    value="tenant_1",
                ),
            ),
        ]
    ),
    query=[0.1, 0.1, 0.9],
    limit=10,
)

성능 고려 사항

이런 방식으로 많은 양의 데이터를 추가하면 각 사용자의 벡터가 같은 컬렉션으로 인덱스되므로 인덱싱 속도가 병목이 될 수 있어요. 이 병목을 피하려면 컬렉션 전체에 대한 전역 벡터 인덱스 구성을 우회하고 개별 그룹에 대해서만 구축하는 것을 고려해 보세요.

이 전략을 채택하면 Qdrant가 각 사용자의 벡터를 독립적으로 인덱스해, 그 과정을 크게 가속화해요.

이 접근을 구현하려면:

  1. HNSW 구성에서 payload_m을 0이 아닌 값(예: 16)으로 설정한다.
  2. hnsw 구성에서 m을 0으로 설정한다. 이렇게 하면 컬렉션 전체에 대한 전역 인덱스 구축이 비활성화된다.
from qdrant_client import QdrantClient, models

client = QdrantClient("localhost", port=6333)

client.create_collection(
    collection_name="{tenant_data}",
    vectors_config=models.VectorParams(size=768, distance=models.Distance.COSINE),
    hnsw_config=models.HnswConfigDiff(
        payload_m=16,
        m=0,
    ),
)
  1. group_id 필드에 대한 키워드 payload 인덱스를 만든다.
client.create_payload_index(
    collection_name="{tenant_data}",
    field_name="group_id",
    field_schema=models.PayloadSchemaType.KEYWORD,
)

참고: 전역 요청(group_id 필터가 없는)은 모든 그룹을 스캔해 최근접 이웃을 찾아야 하므로 더 느려진다는 점을 기억하세요.

Qdrant에서 확장 가능한 솔루션을 위한 멀티테넌시와 커스텀 샤딩 살펴보기

Qdrant는 여러분의 머신러닝 프로젝트를 위한 대규모 아키텍처를 지원할 준비가 되어 있어요. 우리 벡터 데이터베이스가 적합한지 확인해 보려면 quickstart 튜토리얼을 시도하거나 문서와 튜토리얼을 읽어 보세요.

무료 Qdrant 인스턴스를 띄우려면 Qdrant Cloud에 가입하세요 — 조건 없어요.

Discord 커뮤니티에서 지원을 받거나 아이디어를 공유하세요. 벡터 검색 이론을 이야기하고, 예제와 데모를 게시하고, 벡터 데이터베이스 설정을 논의하는 곳이에요.

더 알아보기 (Learn more)