分布式部署

自 v0.8.0 版本以来,Qdrant 支持分布式部署模式。在此模式下,多个 Qdrant 服务相互通信,将数据分布到各个对等节点,以扩展存储能力并提高稳定性。

我应该运行多少个 Qdrant 节点?

理想的 Qdrant 节点数量取决于您在成本节约、弹性以及性能/可伸缩性之间的权衡。

  • 优先考虑成本节约:如果成本对您最重要,请运行单个 Qdrant 节点。不建议在生产环境中使用。缺点:

    • 弹性:在节点重启期间,用户将经历停机时间,除非您有备份或快照,否则无法恢复。
    • 性能:受限于单个服务器的资源。
  • 优先考虑弹性:如果弹性对您最重要,请运行一个包含三个或更多节点且具有两个或更多分片副本的 Qdrant 集群。包含三个或更多节点且具有复制功能的集群,即使一个节点发生故障,也能执行所有操作。此外,它们通过负载均衡获得性能优势,并且可以在一个节点永久丢失的情况下无需备份或快照即可恢复(但仍然强烈建议进行备份)。这最推荐用于生产环境。缺点:

    • 成本:大型集群比小型集群更昂贵,这是此配置的唯一缺点。
  • 平衡成本、弹性和性能:运行一个具有复制分片的双节点 Qdrant 集群,即使一个节点发生故障(例如在维护期间),集群也能响应大多数读/写请求。拥有两个节点也意味着比单节点集群更高的性能,同时仍然比三节点集群更便宜。缺点:

    • 弹性(正常运行时间):当一个节点发生故障时,集群无法对集合执行操作。这些操作需要超过 50% 的节点正常运行,因此这只能在 3 个或更多节点的集群中实现。由于创建、编辑和删除集合通常是罕见的操作,因此许多用户认为这个缺点可以忽略不计。
    • 弹性(数据完整性):如果两个节点中的一个节点上的数据永久丢失或损坏,则除了快照或备份之外无法恢复。只有 3 个或更多节点的集群才能从单个节点的永久丢失中恢复,因为恢复操作需要集群中超过 50% 的节点处于健康状态。
    • 成本:复制分片需要存储两份数据副本。
    • 性能:Qdrant 集群的最大性能随节点数量的增加而提高。

总而言之,单节点集群最适合非生产工作负载,复制的 3 个或更多节点集群是黄金标准,复制的双节点集群则是一个很好的平衡。

在自托管 Qdrant 中启用分布式模式

要启用分布式部署 - 请在配置中启用集群模式或使用环境变量:QDRANT__CLUSTER__ENABLED=true

cluster:
  # Use `enabled: true` to run Qdrant in distributed deployment mode
  enabled: true
  # Configuration of the inter-cluster communication
  p2p:
    # Port for internal communication between peers
    port: 6335

  # Configuration related to distributed consensus algorithm
  consensus:
    # How frequently peers should ping each other.
    # Setting this parameter to lower value will allow consensus
    # to detect disconnected node earlier, but too frequent
    # tick period may create significant network and CPU overhead.
    # We encourage you NOT to change this parameter unless you know what you are doing.
    tick_period_ms: 100

默认情况下,Qdrant 将使用端口 6335 进行内部通信。所有对等节点都应该在集群内部通过此端口访问,但请确保将此端口与外部访问隔离,因为它可能用于执行写入操作。

此外,您必须向第一个对等节点提供 --uri 标志,以便它能够告诉其他节点如何联系它。

./qdrant --uri 'http://qdrant_node_1:6335'

集群中的后续对等节点必须知道现有集群中的至少一个节点,才能通过它与其他集群进行同步。

为此,它们需要提供一个引导 URL。

./qdrant --bootstrap 'http://qdrant_node_1:6335'

新对等节点本身的 URL 将从其请求的 IP 地址自动计算。但也可以使用 --uri 参数单独提供它们。

USAGE:
    qdrant [OPTIONS]

OPTIONS:
        --bootstrap <URI>
            Uri of the peer to bootstrap from in case of multi-peer deployment. If not specified -
            this peer will be considered as a first in a new deployment

        --uri <URI>
            Uri of this peer. Other peers should be able to reach it by this uri.

            This value has to be supplied if this is the first peer in a new deployment.

            In case this is not the first peer and it bootstraps the value is optional. If not
            supplied then qdrant will take internal grpc port from config and derive the IP address
            of this peer on bootstrap peer (receiving side)

成功同步后,您可以通过 REST API 观察集群状态。

GET /cluster

示例结果

{
  "result": {
    "status": "enabled",
    "peer_id": 11532566549086892000,
    "peers": {
      "9834046559507417430": {
        "uri": "http://172.18.0.3:6335/"
      },
      "11532566549086892528": {
        "uri": "http://qdrant_node_1:6335/"
      }
    },
    "raft_info": {
      "term": 1,
      "commit": 4,
      "pending_operations": 1,
      "leader": 11532566549086892000,
      "role": "Leader"
    }
  },
  "status": "ok",
  "time": 5.731e-06
}

请注意,启用分布式模式不会自动复制您的数据。有关后续步骤,请参阅使用新的分布式 Qdrant 集群部分。

在 Qdrant Cloud 中启用分布式模式

为获得最佳效果,请首先确保您的集群正在运行 Qdrant v1.7.4 或更高版本。较旧版本的 Qdrant 确实支持分布式模式,但 v1.7.4 中的改进使分布式集群在中断期间更具弹性。

Qdrant Cloud 控制台中,点击“Scale Up”将集群大小增加到 >1。Qdrant Cloud 会自动配置分布式模式设置。

此外,Qdrant Cloud 还提供自动重新平衡和重新分片集合的功能,这在自托管 Qdrant 中不可用。有关更多详细信息,请参阅 重新分片分片重新平衡部分。

扩容过程完成后,您将有一个新的空节点与现有节点一起运行。要将数据复制到此新空节点中,请参阅下一节。

使用新的分布式 Qdrant 集群

当您启用分布式模式并扩容到两个或更多节点时,您的数据不会自动移动到新节点;它最初是空的。要使用您的新空节点,请执行以下操作之一:

  • 通过将 复制因子设置为 2 或更多,并将 分片数量设置为节点数量的倍数,创建一个新的复制集合。
  • 如果您有一个现有集合,其中不包含足够的分片供每个节点使用,则必须按照上一个要点所述创建一个新集合。
  • 如果您已经有足够的每个节点的分片,并且只需要复制数据,请按照创建新分片副本的说明进行操作。
  • 如果您已经有足够的每个节点的分片,并且数据已经复制,则可以通过移动分片将数据(不复制)移动到新节点。

Raft

Qdrant 使用 Raft 共识协议来维护集群拓扑和集合结构的一致性。

另一方面,点操作不通过共识基础设施。Qdrant 无意提供强事务保证,这使得它能够以低开销执行点操作。实际上,这意味着 Qdrant 不保证原子分布式更新,但允许您等待直到操作完成才能看到写入结果。

相反,集合操作是共识的一部分,这保证了所有操作都是持久的,并最终由所有节点执行。实际上,这意味着在服务执行操作之前,大多数节点会就应应用哪些操作达成一致。

实际上,这意味着如果集群处于转换状态——无论是在故障后选举新领导者还是启动——集合更新操作将被拒绝。

您可以使用集群 REST API 检查共识的状态。

分片

Qdrant 中的集合由一个或多个分片组成。分片是一个独立的点存储,能够执行集合提供的所有操作。有两种将点分布到分片的方法:

  • 自动分片:通过使用一致性哈希算法将点分布到分片中,以便分片管理不相交的点子集。这是默认行为。

  • 用户定义分片自 v1.7.0 起可用 - 每个点都上传到特定的分片,以便操作只命中它们所需的分片。即使采用这种分布方式,分片仍然确保拥有不相交的点子集。查看更多…

每个节点都通过共识协议知道集合的所有部分存储在哪里,因此当您向一个 Qdrant 节点发送搜索请求时,它会自动查询所有其他节点以获取完整的搜索结果。

选择正确的分片数量

创建集合时,Qdrant 会将集合拆分为 shard_number 个分片。如果未设置,shard_number 将设置为创建集合时集群中的节点数量。shard_number 无法在不重新创建集合的情况下更改。

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

client = QdrantClient(url="https://:6333")

client.create_collection(
    collection_name="{collection_name}",
    vectors_config=models.VectorParams(size=300, distance=models.Distance.COSINE),
    shard_number=6,
)
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,
});
use qdrant_client::qdrant::{CreateCollectionBuilder, Distance, VectorParamsBuilder};
use qdrant_client::Qdrant;

let client = Qdrant::from_url("https://:6334").build()?;

client
    .create_collection(
        CreateCollectionBuilder::new("{collection_name}")
            .vectors_config(VectorParamsBuilder::new(300, Distance::Cosine))
            .shard_number(6),
    )
    .await?;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Collections.CreateCollection;
import io.qdrant.client.grpc.Collections.Distance;
import io.qdrant.client.grpc.Collections.VectorParams;
import io.qdrant.client.grpc.Collections.VectorsConfig;

QdrantClient client =
    new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());

client
    .createCollectionAsync(
        CreateCollection.newBuilder()
            .setCollectionName("{collection_name}")
            .setVectorsConfig(
                VectorsConfig.newBuilder()
                    .setParams(
                        VectorParams.newBuilder()
                            .setSize(300)
                            .setDistance(Distance.Cosine)
                            .build())
                    .build())
            .setShardNumber(6)
            .build())
    .get();
using Qdrant.Client;
using Qdrant.Client.Grpc;

var client = new QdrantClient("localhost", 6334);

await client.CreateCollectionAsync(
	collectionName: "{collection_name}",
	vectorsConfig: new VectorParams { Size = 300, Distance = Distance.Cosine },
	shardNumber: 6
);
import (
	"context"

	"github.com/qdrant/go-client/qdrant"
)

client, err := qdrant.NewClient(&qdrant.Config{
	Host: "localhost",
	Port: 6334,
})

client.CreateCollection(context.Background(), &qdrant.CreateCollection{
	CollectionName: "{collection_name}",
	VectorsConfig: qdrant.NewVectorsConfig(&qdrant.VectorParams{
		Size:     300,
		Distance: qdrant.Distance_Cosine,
	}),
	ShardNumber: qdrant.PtrOf(uint32(6)),
})

为确保集群中的所有节点都得到均匀利用,分片数量必须是您当前在集群中运行的节点数量的倍数。

此外:多租户等高级用例可能需要不均匀的分片分布。请参阅多租户

我们建议每个节点至少创建 2 个分片,以便将来扩展而无需重新分片。重新分片在使用我们的云服务时是可能的,但如果托管在其他地方,则应避免,因为它需要创建新集合。

如果您预计会有大量增长,我们建议使用 12 个分片,因为您可以从 1 个节点扩展到 2、3、6 和 12 个节点,而无需重新分片。在小型集群中拥有超过 12 个分片可能不值得性能开销。

当首次创建集合时,分片会在所有现有节点上均匀分布。

当您从集群中添加或移除节点时,现有分片在节点间的重新平衡取决于您如何部署集群。

重新分片

自 v1.13.0 起在云端可用

如果您使用我们的 产品,重新分片允许您更改现有集合中的分片数量。

重新分片可以在不从头开始重新创建集合的情况下,增加或减少分片数量。

更多详细信息,请参阅我们云文档中的重新分片部分。

移动分片

自 v0.9.0 起可用

Qdrant 允许在集群中的节点之间移动分片以及从集群中移除节点。此功能实现了在不停机的情况下动态扩展集群大小的能力。它还允许您在不停机的情况下升级或迁移节点。

如果您的集群在 Qdrant Cloud 中运行,分片会自动在集群节点之间平衡。有关更多信息,请参阅配置云集群云集群扩缩文档。

Qdrant 通过 Collection Cluster info API 提供集群中当前分片分布的信息。

使用 Update collection cluster setup API 启动分片传输。

POST /collections/{collection_name}/cluster
{
    "move_shard": {
        "shard_id": 0,
        "from_peer_id": 381894127,
        "to_peer_id": 467122995
    }
}

传输启动后,服务将根据使用的传输方法处理它,使两个分片保持同步。传输完成后,旧分片将从源节点中删除。

如果您想缩减集群,您可以将所有分片从一个对等节点移开,然后使用 remove peer API 移除该对等节点。

DELETE /cluster/peer/{peer_id}

之后,Qdrant 将把该节点从共识中排除,并且该实例将准备好关闭。

用户定义分片

自 v1.7.0 起可用

Qdrant 允许您为每个点单独指定分片。此功能在您希望控制数据分片位置时非常有用,这样操作就只命中它们实际需要的分片子集。在大型集群中,这可以显著提高不需要扫描整个集合的操作的性能。

此功能的一个明确用例是管理多租户集合,其中每个租户(无论是用户还是组织)都被假定为隔离的,因此他们可以将数据存储在单独的分片中。

要启用用户定义分片,请在集合创建期间将 sharding_method 设置为 custom

PUT /collections/{collection_name}
{
    "shard_number": 1,
    "sharding_method": "custom"
    // ... other collection parameters
}
from qdrant_client import QdrantClient, models

client = QdrantClient(url="https://:6333")

client.create_collection(
    collection_name="{collection_name}",
    shard_number=1,
    sharding_method=models.ShardingMethod.CUSTOM,
    # ... other collection parameters
)
import { QdrantClient } from "@qdrant/js-client-rest";

const client = new QdrantClient({ host: "localhost", port: 6333 });

client.createCollection("{collection_name}", {
    shard_number: 1,
    sharding_method: "custom",
    // ... other collection parameters
});
use qdrant_client::qdrant::{
    CreateCollectionBuilder, Distance, ShardingMethod, VectorParamsBuilder,
};
use qdrant_client::Qdrant;

let client = Qdrant::from_url("https://:6334").build()?;

client
    .create_collection(
        CreateCollectionBuilder::new("{collection_name}")
            .vectors_config(VectorParamsBuilder::new(300, Distance::Cosine))
            .shard_number(1)
            .sharding_method(ShardingMethod::Custom.into()),
    )
    .await?;
import static io.qdrant.client.ShardKeyFactory.shardKey;

import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Collections.CreateCollection;
import io.qdrant.client.grpc.Collections.ShardingMethod;

QdrantClient client =
    new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());

client
    .createCollectionAsync(
        CreateCollection.newBuilder()
            .setCollectionName("{collection_name}")
            // ... other collection parameters
            .setShardNumber(1)
            .setShardingMethod(ShardingMethod.Custom)
            .build())
    .get();
using Qdrant.Client;
using Qdrant.Client.Grpc;

var client = new QdrantClient("localhost", 6334);

await client.CreateCollectionAsync(
	collectionName: "{collection_name}",
	// ... other collection parameters
	shardNumber: 1,
	shardingMethod: ShardingMethod.Custom
);
import (
	"context"

	"github.com/qdrant/go-client/qdrant"
)

client, err := qdrant.NewClient(&qdrant.Config{
	Host: "localhost",
	Port: 6334,
})

client.CreateCollection(context.Background(), &qdrant.CreateCollection{
	CollectionName: "{collection_name}",
	// ... other collection parameters
	ShardNumber:    qdrant.PtrOf(uint32(1)),
	ShardingMethod: qdrant.ShardingMethod_Custom.Enum(),
})

在此模式下,shard_number 表示每个分片键的分片数量,其中点将均匀分布。例如,如果您有 10 个分片键和具有这些设置的集合配置:

{
    "shard_number": 1,
    "sharding_method": "custom",
    "replication_factor": 2
}

那么集合中将总共有 1 * 10 * 2 = 20 个物理分片。

物理分片需要大量资源,因此请确保您的自定义分片键具有较低的基数。

对于基数较大的键,建议改用 按载荷分区

现在您需要创建自定义分片(API 参考

PUT /collections/{collection_name}/shards
{
  "shard_key": "{shard_key}"
}
from qdrant_client import QdrantClient, models

client = QdrantClient(url="https://:6333")

client.create_shard_key("{collection_name}", "{shard_key}")
import { QdrantClient } from "@qdrant/js-client-rest";

const client = new QdrantClient({ host: "localhost", port: 6333 });

client.createShardKey("{collection_name}", {
    shard_key: "{shard_key}"
});
use qdrant_client::qdrant::{
    CreateShardKeyBuilder, CreateShardKeyRequestBuilder
};
use qdrant_client::Qdrant;

let client = Qdrant::from_url("https://:6334").build()?;

client
    .create_shard_key(
        CreateShardKeyRequestBuilder::new("{collection_name}")
            .request(CreateShardKeyBuilder::default().shard_key("{shard_key}".to_string())),
    )
    .await?;
import static io.qdrant.client.ShardKeyFactory.shardKey;

import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Collections.CreateShardKey;
import io.qdrant.client.grpc.Collections.CreateShardKeyRequest;

QdrantClient client =
    new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());

client.createShardKeyAsync(CreateShardKeyRequest.newBuilder()
                .setCollectionName("{collection_name}")
                .setRequest(CreateShardKey.newBuilder()
                                .setShardKey(shardKey("{shard_key}"))
                                .build())
                .build()).get();
using Qdrant.Client;
using Qdrant.Client.Grpc;

var client = new QdrantClient("localhost", 6334);

await client.CreateShardKeyAsync(
    "{collection_name}",
    new CreateShardKey { ShardKey = new ShardKey { Keyword = "{shard_key}", } }
    );
import (
	"context"

	"github.com/qdrant/go-client/qdrant"
)

client, err := qdrant.NewClient(&qdrant.Config{
	Host: "localhost",
	Port: 6334,
})

client.CreateShardKey(context.Background(), "{collection_name}", &qdrant.CreateShardKey{
	ShardKey: qdrant.NewShardKey("{shard_key}"),
})

要为每个点指定分片,您需要在 upsert 请求中提供 shard_key 字段。

PUT /collections/{collection_name}/points
{
    "points": [
        {
            "id": 1111,
            "vector": [0.1, 0.2, 0.3]
        },
    ]
    "shard_key": "user_1"
}
from qdrant_client import QdrantClient, models

client = QdrantClient(url="https://:6333")

client.upsert(
    collection_name="{collection_name}",
    points=[
        models.PointStruct(
            id=1111,
            vector=[0.1, 0.2, 0.3],
        ),
    ],
    shard_key_selector="user_1",
)
import { QdrantClient } from "@qdrant/js-client-rest";

const client = new QdrantClient({ host: "localhost", port: 6333 });

client.upsert("{collection_name}", {
    points: [
        {
            id: 1111,
            vector: [0.1, 0.2, 0.3],
        },
    ],
    shard_key: "user_1",
});
use qdrant_client::qdrant::{PointStruct, UpsertPointsBuilder};
use qdrant_client::Payload;

client
    .upsert_points(
        UpsertPointsBuilder::new(
            "{collection_name}",
            vec![PointStruct::new(
                111,
                vec![0.1, 0.2, 0.3],
                Payload::default(),
            )],
        )
        .shard_key_selector("user_1".to_string()),
    )
    .await?;
import java.util.List;

import static io.qdrant.client.PointIdFactory.id;
import static io.qdrant.client.ShardKeySelectorFactory.shardKeySelector;
import static io.qdrant.client.VectorsFactory.vectors;

import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Points.PointStruct;
import io.qdrant.client.grpc.Points.UpsertPoints;

QdrantClient client =
    new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());

client
    .upsertAsync(
        UpsertPoints.newBuilder()
            .setCollectionName("{collection_name}")
            .addAllPoints(
                List.of(
                    PointStruct.newBuilder()
                        .setId(id(111))
                        .setVectors(vectors(0.1f, 0.2f, 0.3f))
                        .build()))
            .setShardKeySelector(shardKeySelector("user_1"))
            .build()
    )
    .get();
using Qdrant.Client;
using Qdrant.Client.Grpc;

var client = new QdrantClient("localhost", 6334);

await client.UpsertAsync(
	collectionName: "{collection_name}",
	points: new List<PointStruct>
	{
		new() { Id = 111, Vectors = new[] { 0.1f, 0.2f, 0.3f } }
	},
	shardKeySelector: new ShardKeySelector { ShardKeys = { new List<ShardKey> { "user_1" } } }
);
import (
	"context"

	"github.com/qdrant/go-client/qdrant"
)

client, err := qdrant.NewClient(&qdrant.Config{
	Host: "localhost",
	Port: 6334,
})

client.Upsert(context.Background(), &qdrant.UpsertPoints{
	CollectionName: "{collection_name}",
	Points: []*qdrant.PointStruct{
		{
			Id:      qdrant.NewIDNum(111),
			Vectors: qdrant.NewVectors(0.1, 0.2, 0.3),
		},
	},
	ShardKeySelector: &qdrant.ShardKeySelector{
		ShardKeys: []*qdrant.ShardKey{
			qdrant.NewShardKey("user_1"),
		},
	},
})
* 使用自定义分片时,ID 仅在分片键内强制唯一。这意味着如果您有多个具有相同 ID 但分片键不同的点,这是允许的。这是当前实现的一个限制,并且是一种应避免的反模式,因为它可能导致具有相同 ID 的点拥有不同内容的情况。将来,我们计划添加全局 ID 唯一性检查。

现在,您可以通过在执行的任何操作中指定 shard_key 来将操作定位到特定的分片。未指定分片键的操作将对所有分片执行。

另一个用例是让分片按时间顺序跟踪数据,这样您就可以执行更复杂的行程,例如在一个分片中上传实时数据,并在达到一定年龄后将其归档。

Sharding per day

分片传输方法

自 v1.7.0 起可用

有不同的方法可以将分片传输(例如移动或复制)到另一个节点。根据您希望获得的性能和保证以及您希望如何管理集群,您可能希望选择特定的方法。每种方法都有其优缺点。哪种方法最快取决于分片的大小和状态。

可用的分片传输方法有:

  • stream_records(默认)通过分批流式传输其记录到目标节点进行传输。
  • snapshot:通过自动利用快照进行传输,包括其索引和量化数据。
  • wal_delta(自动恢复默认)通过解决WAL差异进行传输;即那些被遗漏的操作。

每种都有优缺点和特定要求,其中一些是:

方法流式记录快照WAL 增量
版本v0.8.0+v1.7.0+v1.8.0+
目标新/现有分片新/现有分片现有分片
连接性内部 gRPC API 6335REST API 6333
内部 gRPC API 6335
内部 gRPC API 6335
HNSW 索引不传输,将在目标上重新索引。传输,在目标上立即可用。不传输,可能在目标上索引。
量化不传输,将在目标上重新量化。传输,在目标上立即可用。不传输,可能在目标上量化。
顺序目标上无序更新1目标上有序更新2目标上有序更新2
磁盘空间无需额外空间两个节点都需要快照的额外空间无需额外空间

要选择分片传输方法,请指定 method,例如:

POST /collections/{collection_name}/cluster
{
    "move_shard": {
        "shard_id": 0,
        "from_peer_id": 381894127,
        "to_peer_id": 467122995,
        "method": "snapshot"
    }
}

stream_records 传输方法是目前最简单的。它只是将所有分片记录分批传输到目标节点,直到全部传输完毕,使两个分片保持同步。它还会确保传输的分片索引过程正在进行,然后才进行最终切换。该方法有两个常见缺点:1. 它不传输索引或量化数据,这意味着分片必须在新节点上重新优化,这可能非常昂贵。2. 排序保证是1,不适合某些应用程序。因为它如此简单,所以它也非常健壮,如果上述缺点在您的用例中可以接受,它是一个可靠的选择。如果您的集群不稳定且资源不足,最好使用 stream_records 传输方法,因为它不太可能失败。

snapshot 传输方法利用快照来传输分片。快照会自动创建。然后将其传输到目标节点并恢复。完成此操作后,快照将从两个节点中删除。当快照/传输/恢复操作发生时,源节点会排队所有新操作。然后,所有排队更新会按顺序发送到目标分片,以使其与源分片处于相同状态。有两个重要的好处:1. 它传输索引和量化数据,因此分片无需在目标节点上再次优化,使其立即可用。这样,Qdrant 确保在传输结束时性能不会下降。特别是在大型分片上,这可以带来巨大的性能提升。2. 排序保证可以是2,这是某些应用程序所必需的。

wal_delta 传输方法只传输两个分片之间的差异。更具体地说,它将所有遗漏的操作传输到目标分片。两个分片的WAL用于解决此问题。它有两个优点:1. 它将非常快,因为它只传输差异而不是所有数据。2. 排序保证可以是2,这是某些应用程序所必需的。两个缺点是:1. 它只能用于传输到已存在于另一个节点上的分片。2. 适用性有限,因为 WAL 通常不包含超过 64MB 的最新操作。但这对于快速重启的节点来说应该足够了,例如用于升级。如果无法解决差异,此方法会自动回退到 stream_records,这相当于传输整个分片。

stream_records 方法目前用作默认方法。未来可能会更改。自 Qdrant 1.9.0 起,wal_delta 用于自动分片复制以恢复死亡分片。

复制

Qdrant 允许您在集群中的节点之间复制分片。

分片复制通过在集群中保留多个分片副本,提高了集群的可靠性。这确保了在节点故障(除非所有副本都丢失)的情况下数据的可用性。

复制因子

创建集合时,您可以通过更改 replication_factor 来控制要存储的分片副本数量。默认情况下,replication_factor 设置为“1”,这意味着不会自动维护额外的副本。默认值可以在 Qdrant 配置中更改。您可以在创建集合时通过设置 replication_factor 来更改它。

可以为现有集合更新 replication_factor,但其效果取决于您运行 Qdrant 的方式。如果您自己托管开源版本的 Qdrant,在集合创建后更改复制因子不会产生任何效果。您可以手动创建或删除分片副本以达到所需的复制因子。在 Qdrant Cloud(包括混合云、私有云)中,您的分片将自动复制或删除以匹配您配置的复制因子。

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

client = QdrantClient(url="https://:6333")

client.create_collection(
    collection_name="{collection_name}",
    vectors_config=models.VectorParams(size=300, distance=models.Distance.COSINE),
    shard_number=6,
    replication_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,
});
use qdrant_client::qdrant::{CreateCollectionBuilder, Distance, VectorParamsBuilder};
use qdrant_client::Qdrant;

let client = Qdrant::from_url("https://:6334").build()?;

client
    .create_collection(
        CreateCollectionBuilder::new("{collection_name}")
            .vectors_config(VectorParamsBuilder::new(300, Distance::Cosine))
            .shard_number(6)
            .replication_factor(2),
    )
    .await?;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Collections.CreateCollection;
import io.qdrant.client.grpc.Collections.Distance;
import io.qdrant.client.grpc.Collections.VectorParams;
import io.qdrant.client.grpc.Collections.VectorsConfig;

QdrantClient client =
    new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());

client
    .createCollectionAsync(
        CreateCollection.newBuilder()
            .setCollectionName("{collection_name}")
            .setVectorsConfig(
                VectorsConfig.newBuilder()
                    .setParams(
                        VectorParams.newBuilder()
                            .setSize(300)
                            .setDistance(Distance.Cosine)
                            .build())
                    .build())
            .setShardNumber(6)
            .setReplicationFactor(2)
            .build())
    .get();
using Qdrant.Client;
using Qdrant.Client.Grpc;

var client = new QdrantClient("localhost", 6334);

await client.CreateCollectionAsync(
	collectionName: "{collection_name}",
	vectorsConfig: new VectorParams { Size = 300, Distance = Distance.Cosine },
	shardNumber: 6,
	replicationFactor: 2
);
import (
	"context"

	"github.com/qdrant/go-client/qdrant"
)

client, err := qdrant.NewClient(&qdrant.Config{
	Host: "localhost",
	Port: 6334,
})

client.CreateCollection(context.Background(), &qdrant.CreateCollection{
	CollectionName: "{collection_name}",
	VectorsConfig: qdrant.NewVectorsConfig(&qdrant.VectorParams{
		Size:     300,
		Distance: qdrant.Distance_Cosine,
	}),
	ShardNumber:       qdrant.PtrOf(uint32(6)),
	ReplicationFactor: qdrant.PtrOf(uint32(2)),
})

此代码示例创建一个集合,总共有 6 个逻辑分片,由总共 12 个物理分片支持。

由于复制因子为“2”将需要两倍的存储空间,因此建议提前确保硬件能够承载额外的分片副本。

创建新的分片副本

可以使用 Update collection cluster setup API 手动在现有集合上创建或删除副本。这通常仅在您运行 Qdrant 开源版本时才需要。在 Qdrant Cloud 中,分片复制会自动处理和更新,以匹配配置的 replication_factor

可以通过指定要从中复制的对等节点,在特定对等节点上添加副本。

POST /collections/{collection_name}/cluster
{
    "replicate_shard": {
        "shard_id": 0,
        "from_peer_id": 381894127,
        "to_peer_id": 467122995
    }
}

并且可以在特定对等节点上删除副本。

POST /collections/{collection_name}/cluster
{
    "drop_replica": {
        "shard_id": 0,
        "peer_id": 381894127
    }
}

请记住,集合必须包含至少一个分片的活动副本。

错误处理

副本可以处于不同的状态:

  • 活动:健康且准备好提供流量
  • 死亡:不健康且未准备好提供流量
  • 部分:目前正在重新同步,等待激活

如果副本不响应内部健康检查或无法提供流量,则将其标记为死亡。

死亡副本将不会接收来自其他对等节点的流量,如果它不自动恢复,可能需要手动干预。

此机制可确保在更新操作期间部分副本失败时的数据一致性和可用性。

节点故障恢复

有时硬件故障可能导致 Qdrant 集群的某些节点无法恢复。没有系统能幸免于此。

但有几种恢复方案允许 Qdrant 保持可用以响应请求,甚至避免性能下降。让我们从最好到最坏的顺序来探讨它们。

使用复制集合进行恢复

如果故障节点数量小于集合的复制因子,则您的集群应该仍然能够执行读取、搜索和更新查询。

现在,如果故障节点重新启动,共识将触发复制过程,以使用它错过的最新更新来更新恢复中的节点。

如果故障节点从未重启,如果您有 3 个或更多节点的集群,则可以恢复丢失的分片。在较小的集群中无法恢复丢失的分片,因为恢复操作通过 raft 进行,而这需要超过 50% 的节点处于健康状态。

使用复制集合重新创建节点

如果一个节点发生故障且无法恢复,您应该将死节点从共识中排除并创建一个空节点。

要将故障节点从共识中排除,请使用 remove peer API。如有必要,请应用 force 标志。

创建新节点时,请确保通过指定 --bootstrap CLI 参数和任何运行中集群节点的 URL 将其附加到现有集群。

一旦新节点准备就绪并与集群同步,您可能需要确保集合分片已充分复制。请记住,Qdrant 不会自动平衡分片,因为这是一项昂贵的操作。使用 Replicate Shard Operation 在新连接的节点上创建分片的另一个副本。

值得一提的是,Qdrant 仅提供了创建自动化故障恢复所需的构建块。构建一个完全自动化的集合扩缩过程需要对集群机器本身进行控制。请查看我们的 云解决方案,我们正是这样做的。

从快照恢复

如果集群中没有数据副本,仍然可以从快照恢复。

按照相同的步骤分离故障节点并在集群中创建新节点。

  • 要将故障节点从共识中排除,请使用 remove peer API。如有必要,请应用 force 标志。
  • 创建一个新节点,确保通过指定 --bootstrap CLI 参数和任何运行中集群节点的 URL 将其附加到现有集群。

单节点部署中使用的快照恢复与集群部署中的不同。共识管理所有集合的所有元数据,并且不需要快照来恢复它。但您可以使用快照来恢复丢失的集合分片。

使用 Collection Snapshot Recovery API 来执行此操作。服务将下载指定集合的快照并从中恢复带有数据的分片。

一旦集合的所有分片都恢复,集合将再次运行。

临时节点故障

如果配置得当,在分布式模式下运行 Qdrant 可以使您的集群在单个节点暂时发生故障时抵御中断。

以下是不同配置的 Qdrant 集群如何响应:

  • 1 节点集群:所有操作都会超时或失败,长达几分钟。这取决于重启和从磁盘加载数据需要多长时间。
  • 2 节点集群(分片未复制):所有操作都会超时或失败,长达几分钟。这取决于重启和从磁盘加载数据需要多长时间。
  • 2 节点集群(所有分片都复制到两个节点):除了集合上的操作外,所有请求在中断期间继续工作。
  • 3+ 节点集群(所有分片都复制到至少 2 个节点):所有请求在中断期间继续工作。

一致性保证

默认情况下,Qdrant 专注于搜索操作的可用性和最大吞吐量。对于大多数用例来说,这是一种更可取的权衡。

在正常运行状态下,可以从集群中的任何对等节点搜索和修改数据。

在响应客户端之前,处理请求的对等节点根据当前拓扑分派所有操作,以保持数据在集群中同步。

  • 读取使用部分扇出策略来优化延迟和可用性
  • 写入并行执行于所有活动的已分片副本

默认情况下,对一个点的并发更新可能导致不一致的状态。例如,如果两个客户端同时更新集合中具有三个分片副本的同一个点。在某些副本上,该点可能反映一个客户端的更新,而在其他副本上,该点可能反映另一个客户端的更新。

Two clients updating the same point at the same time.

在某些情况下,在可能出现硬件不稳定、同一文档大规模并发更新等情况下,需要确保额外的保证。

Qdrant 提供了几个选项来控制一致性保证:

  • write_consistency_factor - 定义在响应客户端之前必须确认写入操作的副本数量。增加此值将使写入操作能够容忍集群中的网络分区,但将需要更多活动副本才能执行写入操作。
  • Read consistency 参数,可与搜索和检索操作一起使用,以确保从所有副本获得的结果相同。如果使用此选项,Qdrant 将在多个副本上执行读取操作,并根据所选策略解析结果。此选项有助于避免同一文档并发更新时的数据不一致。如果更新操作频繁且副本数量较少,则优先选择此选项。
  • Write ordering 参数,可与更新和删除操作一起使用,以确保操作在所有副本上以相同的顺序执行。如果使用此选项,Qdrant 会将操作路由到分片的领导副本,并等待响应后再响应客户端。此选项有助于避免同一文档并发更新时的数据不一致。如果读取操作比更新操作频繁且搜索性能至关重要,则优先选择此选项。

写入一致性因子

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="https://: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,
});
use qdrant_client::qdrant::{CreateCollectionBuilder, Distance, VectorParamsBuilder};
use qdrant_client::Qdrant;

let client = Qdrant::from_url("https://:6334").build()?;

client
    .create_collection(
        CreateCollectionBuilder::new("{collection_name}")
            .vectors_config(VectorParamsBuilder::new(300, Distance::Cosine))
            .shard_number(6)
            .replication_factor(2)
            .write_consistency_factor(2),
    )
    .await?;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Collections.CreateCollection;
import io.qdrant.client.grpc.Collections.Distance;
import io.qdrant.client.grpc.Collections.VectorParams;
import io.qdrant.client.grpc.Collections.VectorsConfig;

QdrantClient client =
    new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());

client
    .createCollectionAsync(
        CreateCollection.newBuilder()
            .setCollectionName("{collection_name}")
            .setVectorsConfig(
                VectorsConfig.newBuilder()
                    .setParams(
                        VectorParams.newBuilder()
                            .setSize(300)
                            .setDistance(Distance.Cosine)
                            .build())
                    .build())
            .setShardNumber(6)
            .setReplicationFactor(2)
            .setWriteConsistencyFactor(2)
            .build())
    .get();
using Qdrant.Client;
using Qdrant.Client.Grpc;

var client = new QdrantClient("localhost", 6334);

await client.CreateCollectionAsync(
	collectionName: "{collection_name}",
	vectorsConfig: new VectorParams { Size = 300, Distance = Distance.Cosine },
	shardNumber: 6,
	replicationFactor: 2,
	writeConsistencyFactor: 2
);
import (
	"context"

	"github.com/qdrant/go-client/qdrant"
)

client, err := qdrant.NewClient(&qdrant.Config{
	Host: "localhost",
	Port: 6334,
})

client.CreateCollection(context.Background(), &qdrant.CreateCollection{
	CollectionName: "{collection_name}",
	VectorsConfig: qdrant.NewVectorsConfig(&qdrant.VectorParams{
		Size:     300,
		Distance: qdrant.Distance_Cosine,
	}),
	ShardNumber:            qdrant.PtrOf(uint32(6)),
	ReplicationFactor:      qdrant.PtrOf(uint32(2)),
	WriteConsistencyFactor: qdrant.PtrOf(uint32(2)),
})

如果活动副本数量少于 write_consistency_factor,写入操作将失败。在这种情况下,客户端需要重新发送操作以确保达到一致状态。

write_consistency_factor 设置为较低值可能允许接受写入,即使存在无响应的节点。无响应的节点被标记为死亡,并在可用时自动恢复以确保数据一致性。

write_consistency_factor 的配置对于调整集群在某些节点因重启、升级或故障而脱机时的行为非常重要。

默认情况下,只要每个分片至少有一个副本在线,集群就会继续接受更新。然而,这种行为意味着一旦脱机副本恢复,它将需要与集群的其余部分进行额外的同步。在某些情况下,这种同步可能资源密集且不希望发生。

write_consistency_factor 设置为与复制因子匹配会修改集群的行为,从而拒绝未复制的更新,从而无需额外同步。

如果更新已应用于足够的副本(根据 write_consistency_factor),则更新将返回成功状态。任何未能应用更新的副本将被暂时禁用,并自动恢复以保持数据一致性。如果更新无法应用于足够的副本,它将返回错误并且可能部分应用。用户必须重新提交操作以确保数据一致性。

对于能够处理错误和重试的异步更新和注入管道,此策略可能更可取。

读取一致性

读取 consistency 可以为大多数读取请求指定,并将确保返回的结果在集群节点之间保持一致。

  • all 将查询所有节点并返回所有节点上存在的点。
  • majority 将查询所有节点并返回大多数节点上存在的点。
  • quorum 将查询随机选择的大多数节点并返回所有这些节点上存在的点。
  • 1/2/3/等 - 将查询指定数量的随机选择的节点并返回所有这些节点上存在的点。
  • 默认 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",
});
use qdrant_client::qdrant::{
    read_consistency::Value, Condition, Filter, QueryPointsBuilder, ReadConsistencyType,
    SearchParamsBuilder,
};
use qdrant_client::{Qdrant, QdrantError};

let client = Qdrant::from_url("https://:6334").build()?;

client
    .query(
        QueryPointsBuilder::new("{collection_name}")
            .query(vec![0.2, 0.1, 0.9, 0.7])
            .limit(3)
            .filter(Filter::must([Condition::matches(
                "city",
                "London".to_string(),
            )]))
            .params(SearchParamsBuilder::default().hnsw_ef(128).exact(false))
            .read_consistency(Value::Type(ReadConsistencyType::Majority.into())),
    )
    .await?;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Common.Filter;
import io.qdrant.client.grpc.Points.QueryPoints;
import io.qdrant.client.grpc.Points.ReadConsistency;
import io.qdrant.client.grpc.Points.ReadConsistencyType;
import io.qdrant.client.grpc.Points.SearchParams;

import static io.qdrant.client.QueryFactory.nearest;
import static io.qdrant.client.ConditionFactory.matchKeyword;

QdrantClient client =
    new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());

client.queryAsync(
        QueryPoints.newBuilder()
                .setCollectionName("{collection_name}")
                .setFilter(Filter.newBuilder().addMust(matchKeyword("city", "London")).build())
                .setQuery(nearest(.2f, 0.1f, 0.9f, 0.7f))
                .setParams(SearchParams.newBuilder().setHnswEf(128).setExact(false).build())
                .setLimit(3)
                .setReadConsistency(
                        ReadConsistency.newBuilder().setType(ReadConsistencyType.Majority).build())
                .build())
        .get();
using Qdrant.Client;
using Qdrant.Client.Grpc;
using static Qdrant.Client.Grpc.Conditions;

var client = new QdrantClient("localhost", 6334);

await client.QueryAsync(
	collectionName: "{collection_name}",
	query: new float[] { 0.2f, 0.1f, 0.9f, 0.7f },
	filter: MatchKeyword("city", "London"),
	searchParams: new SearchParams { HnswEf = 128, Exact = false },
	limit: 3,
	readConsistency: new ReadConsistency { Type = ReadConsistencyType.Majority }
);
import (
	"context"

	"github.com/qdrant/go-client/qdrant"
)

client, err := qdrant.NewClient(&qdrant.Config{
	Host: "localhost",
	Port: 6334,
})

client.Query(context.Background(), &qdrant.QueryPoints{
	CollectionName: "{collection_name}",
	Query:          qdrant.NewQuery(0.2, 0.1, 0.9, 0.7),
	Filter: &qdrant.Filter{
		Must: []*qdrant.Condition{
			qdrant.NewMatch("city", "London"),
		},
	},
	Params: &qdrant.SearchParams{
		HnswEf: qdrant.PtrOf(uint64(128)),
	},
	Limit:           qdrant.PtrOf(uint64(3)),
	ReadConsistency: qdrant.NewReadConsistencyType(qdrant.ReadConsistencyType_Majority),
})

写入顺序

写入 ordering 可以为任何写入请求指定,以通过单个“领导者”节点将其序列化,这确保所有写入操作(使用相同的 ordering 发出)按顺序执行和观察。

  • weak (默认)排序不提供任何额外保证,因此写入操作可以自由重新排序。
  • medium 排序通过动态选举的领导者序列化所有写入操作,这可能在领导者更改时导致轻微不一致。
  • strong 排序通过永久领导者序列化所有写入操作,这提供了强一致性,但如果领导者宕机,写入操作可能不可用。
PUT /collections/{collection_name}/points?ordering=strong
{
    "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]
        ]
    }
}
client.upsert(
    collection_name="{collection_name}",
    points=models.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=models.WriteOrdering.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",
});
use qdrant_client::qdrant::{
    PointStruct, UpsertPointsBuilder, WriteOrdering, WriteOrderingType
};
use qdrant_client::Qdrant;

let client = Qdrant::from_url("https://:6334").build()?;

client
    .upsert_points(
        UpsertPointsBuilder::new(
            "{collection_name}",
            vec![
                PointStruct::new(1, vec![0.9, 0.1, 0.1], [("color", "red".into())]),
                PointStruct::new(2, vec![0.1, 0.9, 0.1], [("color", "green".into())]),
                PointStruct::new(3, vec![0.1, 0.1, 0.9], [("color", "blue".into())]),
            ],
        )
        .ordering(WriteOrdering {
            r#type: WriteOrderingType::Strong.into(),
        }),
    )
    .await?;
import java.util.List;
import java.util.Map;

import static io.qdrant.client.PointIdFactory.id;
import static io.qdrant.client.ValueFactory.value;
import static io.qdrant.client.VectorsFactory.vectors;

import io.qdrant.client.grpc.Points.PointStruct;
import io.qdrant.client.grpc.Points.UpsertPoints;
import io.qdrant.client.grpc.Points.WriteOrdering;
import io.qdrant.client.grpc.Points.WriteOrderingType;

client
    .upsertAsync(
        UpsertPoints.newBuilder()
            .setCollectionName("{collection_name}")
            .addAllPoints(
                List.of(
                    PointStruct.newBuilder()
                        .setId(id(1))
                        .setVectors(vectors(0.9f, 0.1f, 0.1f))
                        .putAllPayload(Map.of("color", value("red")))
                        .build(),
                    PointStruct.newBuilder()
                        .setId(id(2))
                        .setVectors(vectors(0.1f, 0.9f, 0.1f))
                        .putAllPayload(Map.of("color", value("green")))
                        .build(),
                    PointStruct.newBuilder()
                        .setId(id(3))
                        .setVectors(vectors(0.1f, 0.1f, 0.94f))
                        .putAllPayload(Map.of("color", value("blue")))
                        .build()))
            .setOrdering(WriteOrdering.newBuilder().setType(WriteOrderingType.Strong).build())
            .build())
    .get();
using Qdrant.Client;
using Qdrant.Client.Grpc;

var client = new QdrantClient("localhost", 6334);

await client.UpsertAsync(
	collectionName: "{collection_name}",
	points: new List<PointStruct>
	{
		new()
		{
			Id = 1,
			Vectors = new[] { 0.9f, 0.1f, 0.1f },
			Payload = { ["color"] = "red" }
		},
		new()
		{
			Id = 2,
			Vectors = new[] { 0.1f, 0.9f, 0.1f },
			Payload = { ["color"] = "green" }
		},
		new()
		{
			Id = 3,
			Vectors = new[] { 0.1f, 0.1f, 0.9f },
			Payload = { ["color"] = "blue" }
		}
	},
	ordering: WriteOrderingType.Strong
);
import (
	"context"

	"github.com/qdrant/go-client/qdrant"
)

client, err := qdrant.NewClient(&qdrant.Config{
	Host: "localhost",
	Port: 6334,
})

client.Upsert(context.Background(), &qdrant.UpsertPoints{
	CollectionName: "{collection_name}",
	Points: []*qdrant.PointStruct{
		{
			Id:      qdrant.NewIDNum(1),
			Vectors: qdrant.NewVectors(0.9, 0.1, 0.1),
			Payload: qdrant.NewValueMap(map[string]any{"color": "red"}),
		},
		{
			Id:      qdrant.NewIDNum(2),
			Vectors: qdrant.NewVectors(0.1, 0.9, 0.1),
			Payload: qdrant.NewValueMap(map[string]any{"color": "green"}),
		},
		{
			Id:      qdrant.NewIDNum(3),
			Vectors: qdrant.NewVectors(0.1, 0.1, 0.9),
			Payload: qdrant.NewValueMap(map[string]any{"color": "blue"}),
		},
	},
	Ordering: &qdrant.WriteOrdering{
		Type: qdrant.WriteOrderingType_Strong,
	},
})

监听器模式

在某些情况下,拥有一个只积累数据而不参与搜索操作的 Qdrant 节点可能很有用。在以下几种场景中这可能很有用:

  • 监听器选项可用于将数据存储在单独的节点中,该节点可用于备份或长时间存储数据。
  • 监听器节点可用于将数据同步到另一个区域,同时仍在本地区域执行搜索操作。

要启用监听器模式,请在配置文件中将 node_type 设置为 Listener

storage:
  node_type: "Listener"

监听器节点将不参与搜索操作,但仍将接受写入操作并将数据存储在本地存储中。

存储在监听器节点上的所有分片都将转换为 Listener 状态。

此外,发送到监听器节点的所有写入请求都将使用 wait=false 选项处理,这意味着写入操作一旦写入 WAL,即被视为成功。这种机制应该能够在并行快照的情况下最小化 upsert 延迟。

共识检查点

共识检查点是 Raft 中用于通过定期创建系统状态的一致快照来提高性能和简化日志管理的技术。此快照代表集群中所有节点就状态达成一致的时间点,可用于截断日志,减少需要存储和在节点之间传输的数据量。

例如,如果您将新节点附加到集群,它应该重放所有日志条目以赶上当前状态。在长期运行的集群中,这可能需要很长时间,并且日志可能会变得非常大。

为了防止这种情况,可以使用特殊的检查点机制,该机制将截断日志并创建当前状态的快照。

要使用此功能,只需在所需节点上调用 /cluster/recover API。

POST /cluster/recover

此 API 可以在任何非领导者节点上触发,它将向当前共识领导者发送请求以创建快照。领导者反过来会将快照发送回请求节点以供应用。

在某些情况下,此 API 可用于通过强制创建快照来从不一致的集群状态中恢复。


  1. 更新的弱排序:所有记录按顺序流式传输到目标节点。在记录传输仍在进行时,新更新并行接收到目标节点。因此,无论更新使用何种排序,我们都具有排序。 ↩︎ ↩︎

  2. 更新的强排序:创建分片快照,传输并在目标节点上恢复。这确保了分片状态保持一致。新更新在源节点上排队,并按顺序传输到目标节点。因此,更新具有用户选择的相同排序,从而实现排序。 ↩︎ ↩︎ ↩︎ ↩︎

此页面有用吗?

感谢您的反馈!🙏

很抱歉听到这个消息。😔 您可以在 GitHub 上编辑此页面,或创建 GitHub Issue。