일관성 보장

일관성 보장 (Consistency Guarantees)

출처: Qdrant 공식 문서 — consistency-guarantees

여러분, 분산 시스템에서 일관성(consistency)은 언제나 다루기 까다로운 주제예요. 기본적으로 Qdrant는 가용성(availability)과 최대 검색 처리량에 초점을 맞춰요. 이는 대부분의 사용 사례에서 선호되는 trade-off예요. 정상 운영 중에는 클러스터의 어느 피어(peer)에서든 데이터를 검색하고 수정할 수 있어요. 읽기는 지연과 가용성을 최적화하기 위해 부분 fan-out 전략을 쓰고, 쓰기는 모든 활성 샤드 레플리카에서 병렬로 실행돼요.

이 말은 한 point에 대한 동시 업데이트가 불일치 상태를 만들 수 있다는 뜻이에요. 예를 들어 샤드당 3개의 레플리카가 있는 컬렉션에서 두 클라이언트가 동시에 같은 point를 업데이트한다고 해볼게요. 어떤 레플리카에서는 그 point가 한 클라이언트의 업데이트를 반영하고, 다른 레플리카에서는 다른 클라이언트의 업데이트를 반영할 수 있어요.

경우에 따라서는 하드웨어 불안정, 동일 문서에 대한 대량 동시 업데이트 등이 발생할 때 추가적인 보장이 필요해요.

Qdrant는 일관성 보장을 제어하기 위한 몇 가지 옵션을 제공해요:

  • write_consistency_factor — 클라이언트에게 응답하기 전에 쓰기 연산을 승인(acknowledge)해야 하는 레플리카 수를 정의해요. 이 값을 늘리면 쓰기 연산이 클러스터의 네트워크 파티션에 강건해지지만, 쓰기 연산을 수행하려면 더 많은 활성 레플리카가 필요해져요.
  • 읽기 consistency 매개변수 — 검색·조회 연산과 함께 사용해 모든 레플리카에서 얻은 결과가 동일함을 보장할 수 있어요. 이 옵션을 쓰면 Qdrant가 여러 레플리카에서 읽기 연산을 수행하고 선택한 전략에 따라 결과를 해석해요. 동일 문서의 동시 업데이트 시 데이터 불일치를 피하는 데 유용해요. 업데이트 연산이 빈번하고 레플리카 수가 적을 때 선호돼요.
  • 쓰기 ordering 매개변수 — 업데이트·삭제 연산과 함께 사용해 모든 레플리카에서 그 연산이 같은 순서로 실행됨을 보장할 수 있어요. 이 옵션을 쓰면 Qdrant가 연산을 샤드의 리더 레플리카로 라우팅하고, 클라이언트에게 응답하기 전에 응답을 기다려요. 동일 문서의 동시 업데이트 시 데이터 불일치를 피하는 데 유용해요. 읽기 연산이 업데이트보다 빈번하고 검색 성능이 중요할 때 선호돼요.

쓰기 일관성 계수 (Write Consistency Factor)

write_consistency_factor는 클라이언트에게 응답하기 전에 쓰기 연산을 승인해야 하는 레플리카 수를 나타내요. 기본값은 1이에요. 컬렉션 생성 시 또는 컬렉션 매개변수 업데이트 시 설정할 수 있어요.

이 값은 1부터 각 샤드의 레플리카 수까지의 범위를 가질 수 있어요.

PUT /collections/{collection_name}
{
    "vectors": {
        "size": 300,
        "distance": "Cosine"
    },
    "shard_number": 6,
    "replication_factor": 2,
    "write_consistency_factor": 2
}
from qdrant_client import QdrantClient, models

client = QdrantClient(url="http://localhost:6333")

client.create_collection(
    collection_name="{collection_name}",
    vectors_config=models.VectorParams(size=300, distance=models.Distance.COSINE),
    shard_number=6,
    replication_factor=2,
    write_consistency_factor=2,
)
import { QdrantClient } from "@qdrant/js-client-rest";

const client = new QdrantClient({ host: "localhost", port: 6333 });
client.createCollection("{collection_name}", {
  vectors: { size: 300, distance: "Cosine" },
  shard_number: 6,
  replication_factor: 2,
  write_consistency_factor: 2,
});

활성 레플리카 수가 write_consistency_factor보다 적으면 쓰기 연산은 실패해요. 이 경우 클라이언트는 일관된 상태에 도달하도록 연산을 다시 보내야 해요.

write_consistency_factor를 낮은 값으로 설정하면 응답하지 않는 노드가 있어도 쓰기를 수용할 수 있어요. 응답하지 않는 노드는 죽은 것으로 표시되고, 데이터 일관성을 보장하기 위해 다시 사용 가능해지면 자동으로 복구돼요.

write_consistency_factor 설정은 재시작, 업그레이드, 장애로 일부 노드가 오프라인이 될 때 클러스터의 동작을 조정하는 데 중요해요.

기본적으로 클러스터는 각 샤드의 레플리카가 하나 이상 온라인인 동안 업데이트를 계속 수용해요. 하지만 이 동작은 오프라인 레플리카가 복구되면 클러스터의 나머지와 추가 동기화가 필요함을 의미해요. 어떤 경우에는 이 동기화가 리소스 집약적이고 바람직하지 않을 수 있어요.

write_consistency_factor를 복제 계수(replication factor)와 같게 설정하면, 복제되지 않은 업데이트가 거부되도록 클러스터 동작이 바뀌어 추가 동기화를 방지해요.

업데이트가 write_consistency_factor에 따라 충분한 레플리카에 적용되면 성공 상태를 반환해요. 업데이트를 적용하지 못한 레플리카는 일시적으로 비활성화되고, 데이터 일관성을 유지하기 위해 자동 복구돼요. 충분한 레플리카에 적용하지 못했다면 오류를 반환하고 부분 적용될 수 있어요. 데이터 일관성을 보장하려면 사용자가 연산을 다시 제출해야 해요.

비동기 업데이트와 오류·재시도를 처리할 수 있는 주입 파이프라인의 경우 이 전략이 더 선호될 수 있어요.

읽기 일관성 (Read Consistency)

읽기 consistency는 대부분의 읽기 요청에서 지정할 수 있고, 반환된 결과가 클러스터 노드 간 일관되도록 보장해요.

  • all — 모든 노드를 조회하고, 그 모두에 존재하는 point를 반환해요.
  • majority — 모든 노드를 조회하고, 그 대부분에 존재하는 point를 반환해요.
  • quorum — 무작위로 선택된 과반수 노드를 조회하고, 그 모두에 존재하는 point를 반환해요.
  • 1/2/3 등 — 지정된 수의 무작위 선택 노드를 조회하고, 그 모두에 존재하는 point를 반환해요.
  • 기본 consistency1이에요.
POST /collections/{collection_name}/points/query?consistency=majority
{
    "query": [0.2, 0.1, 0.9, 0.7],
    "filter": {
        "must": [
            { "key": "city", "match": { "value": "London" } }
        ]
    },
    "params": { "hnsw_ef": 128, "exact": false },
    "limit": 3
}
client.query_points(
    collection_name="{collection_name}",
    query=[0.2, 0.1, 0.9, 0.7],
    query_filter=models.Filter(
        must=[
            models.FieldCondition(
                key="city",
                match=models.MatchValue(value="London"),
            )
        ]
    ),
    search_params=models.SearchParams(hnsw_ef=128, exact=False),
    limit=3,
    consistency="majority",
)
client.query("{collection_name}", {
  query: [0.2, 0.1, 0.9, 0.7],
  filter: { must: [{ key: "city", match: { value: "London" } }] },
  params: { hnsw_ef: 128, exact: false },
  limit: 3,
  consistency: "majority",
});

쓰기 순서 (Write Ordering)

동일 point에 대한 동시 업데이트 순서가 문제가 될 수 있어요. 쓰기 ordering을 사용하면 모든 레플리카에서 업데이트·삭제 연산이 같은 순서로 실행됨을 보장할 수 있어요. 이 옵션을 쓰면 Qdrant가 연산을 샤드의 리더 레플리카로 라우팅하고, 클라이언트에게 응답하기 전에 응답을 기다려요.

client.upsert(
    collection_name="{collection_name}",
    points=[
        models.PointStruct(id=1, vector=[0.9, 0.1, 0.1], payload={"color": "red"}),
        models.PointStruct(id=2, vector=[0.1, 0.9, 0.1], payload={"color": "green"}),
        models.PointStruct(id=3, vector=[0.1, 0.1, 0.9], payload={"color": "blue"}),
    ],
    ordering=models.WriteOrdering(type=models.WriteOrderingType.STRONG),
)
client.upsert("{collection_name}", {
  batch: {
    ids: [1, 2, 3],
    payloads: [{ color: "red" }, { color: "green" }, { color: "blue" }],
    vectors: [
      [0.9, 0.1, 0.1],
      [0.1, 0.9, 0.1],
      [0.1, 0.1, 0.9],
    ],
  },
  ordering: "strong",
});

ordering 옵션을 쓰면 모든 레플리카에서 업데이트·삭제 연산이 같은 순서로 실행됨을 보장할 수 있고, 쓰기가 리더를 통해 직렬화되므로 지연이 증가할 수 있어요. 특히 같은 문서에 대한 동시 업데이트가 잦고 쓰기 연산의 순서가 중요할 때 유용해요.

더 알아보기 (Learn more)