milvus-logo
LFAI
Home
  • Guide d'administration
    • Groupes de ressources

Gestion des groupes de ressources

Dans Milvus, vous pouvez utiliser un groupe de ressources pour isoler physiquement certains nœuds de requête des autres. Ce guide vous explique comment créer et gérer des groupes de ressources personnalisés et comment transférer des nœuds entre eux.

Qu'est-ce qu'un groupe de ressources ?

Un groupe de ressources peut contenir plusieurs ou tous les nœuds de requête d'un cluster Milvus. Vous décidez de la manière dont vous souhaitez répartir les nœuds de requête entre les groupes de ressources en fonction de ce qui vous semble le plus judicieux. Par exemple, dans un scénario à plusieurs collections, vous pouvez allouer un nombre approprié de nœuds de requête à chaque groupe de ressources et charger les collections dans différents groupes de ressources, de sorte que les opérations au sein de chaque collection soient physiquement indépendantes de celles des autres collections.

Notez qu'une instance Milvus maintient un groupe de ressources par défaut pour contenir tous les nœuds de requête au démarrage et le nomme __default_resource_group.

À partir de la version 2.4.1, Milvus fournit une API de groupe de ressources déclarative, tandis que l'ancienne API de groupe de ressources a été supprimée. La nouvelle API déclarative permet aux utilisateurs d'atteindre l'idempotence, afin de faciliter le développement secondaire dans les environnements "cloud-native".

Concepts du groupe de ressources

Un groupe de ressources est décrit par une configuration de groupe de ressources :

{
    "requests": { "nodeNum": 1 },
    "limits": { "nodeNum": 1 },
    "transfer_from": [{ "resource_group": "rg1" }],
    "transfer_to": [{ "resource_group": "rg2" }]
}
  • L'attribut requests spécifie les conditions qu'un groupe de ressources doit remplir.
  • L'attribut limits spécifie les limites maximales d'un groupe de ressources.
  • Les attributs transfer_from et transfer_to décrivent respectivement les groupes de ressources à partir desquels un groupe de ressources doit de préférence acquérir des ressources et les groupes de ressources auxquels il doit transférer des ressources.

Lorsque la configuration d'un groupe de ressources change, le Milvus ajuste autant que possible les ressources du nœud de requête actuel en fonction de la nouvelle configuration, en veillant à ce que tous les groupes de ressources remplissent finalement la condition suivante :

.requests.nodeNum < nodeNumOfResourceGroup < .limits.nodeNum.

Sauf dans les cas suivants :

  • Lorsque le nombre de QueryNodes dans le cluster Milvus est insuffisant, c'est-à-dire NumOfQueryNode < sum(.requests.nodeNum), il y aura toujours des groupes de ressources sans suffisamment de QueryNodes.
  • Lorsque le nombre de QueryNodes dans le cluster Milvus est excessif, c'est-à-dire NumOfQueryNode > sum(.limits.nodeNum), les QueryNodes redondants seront toujours placés en premier dans le __default_resource_group.

Bien entendu, si le nombre de QueryNodes dans le cluster change, Milvus tentera continuellement de s'adapter pour répondre aux conditions finales. Par conséquent, vous pouvez d'abord appliquer les modifications de configuration du groupe de ressources, puis procéder à la mise à l'échelle des QueryNodes.

Utiliser l'API déclarative pour gérer le groupe de ressources

Tous les exemples de code sur cette page sont dans PyMilvus 2.5.0. Mettez à jour votre installation PyMilvus avant de les exécuter.

  1. Créer un groupe de ressources.

    Pour créer un groupe de ressources, exécutez ce qui suit après vous être connecté à une instance Milvus. L'extrait suivant suppose que default est l'alias de votre connexion Milvus.

    import pymilvus
    
    # A resource group name should be a string of 1 to 255 characters, starting with a letter or an underscore (_) and containing only numbers, letters, and underscores (_).
    name = "rg"
    node_num = 0
    
    # create a resource group that exactly hold no query node.
    try:
        utility.create_resource_group(name, config=utility.ResourceGroupConfig(
            requests={"node_num": node_num},
            limits={"node_num": node_num},
        ), using='default')
        print(f"Succeeded in creating resource group {name}.")
    except Exception:
        print("Failed to create the resource group.")
    
  2. Répertorier les groupes de ressources.

    Une fois que vous avez créé un groupe de ressources, vous pouvez le voir dans la liste des groupes de ressources.

    Pour afficher la liste des groupes de ressources dans une instance Milvus, procédez comme suit :

    rgs = utility.list_resource_groups(using='default')
    print(f"Resource group list: {rgs}")
    
    # Resource group list: ['__default_resource_group', 'rg']
    
  3. Décrire un groupe de ressources.

    Vous pouvez demander à Milvus de décrire un groupe de ressources en procédant comme suit :

    info = utility.describe_resource_group(name, using="default")
    print(f"Resource group description: {info}")
    
    # Resource group description: 
    #        <name:"rg">,           // string, rg name
    #        <capacity:1>,            // int, num_node which has been transfer to this rg
    #        <num_available_node:0>,  // int, available node_num, some node may shutdown
    #        <num_loaded_replica:{}>, // map[string]int, from collection_name to loaded replica of each collecion in this rg
    #        <num_outgoing_node:{}>,  // map[string]int, from collection_name to outgoging accessed node num by replica loaded in this rg 
    #        <num_incoming_node:{}>.  // map[string]int, from collection_name to incoming accessed node num by replica loaded in other rg
    
  4. Transférer des nœuds entre les groupes de ressources.

    Vous remarquerez peut-être que le groupe de ressources décrit n'a pas encore de nœud de requête. Déplacez certains nœuds du groupe de ressources par défaut vers celui que vous créez comme suit : Supposons qu'il y ait actuellement 1 QueryNodes dans le __default_resource_group du cluster et que nous voulions transférer un nœud dans le rg créé.update_resource_groups garantit l'atomicité pour plusieurs changements de configuration, de sorte qu'aucun état intermédiaire ne sera visible par Milvus.

    source = '__default_resource_group'
    target = 'rg'
    expected_num_nodes_in_default = 0
    expected_num_nodes_in_rg = 1
    
    try:
        utility.update_resource_groups({
            source: ResourceGroupConfig(
                requests={"node_num": expected_num_nodes_in_default},
                limits={"node_num": expected_num_nodes_in_default},
            ),
            target: ResourceGroupConfig(
                requests={"node_num": expected_num_nodes_in_rg},
                limits={"node_num": expected_num_nodes_in_rg},
            )
        }, using="default")
        print(f"Succeeded in move 1 node(s) from {source} to {target}.")
    except Exception:
        print("Something went wrong while moving nodes.")
    
    # After a while, succeeded in moving 1 node(s) from __default_resource_group to rg.
    
  5. Chargement de collections et de partitions dans un groupe de ressources.

    Une fois qu'il y a des nœuds de requête dans un groupe de ressources, vous pouvez charger des collections dans ce groupe de ressources. L'extrait suivant suppose qu'une collection nommée demo existe déjà.

    from pymilvus import Collection
    
    collection = Collection('demo')
    
    # Milvus loads the collection to the default resource group.
    collection.load(replica_number=2)
    
    # Or, you can ask Milvus load the collection to the desired resource group.
    # make sure that query nodes num should be greater or equal to replica_number
    resource_groups = ['rg']
    collection.load(replica_number=2, _resource_groups=resource_groups) 
    

    Vous pouvez également charger une partition dans un groupe de ressources et répartir ses répliques entre plusieurs groupes de ressources. L'extrait suivant suppose qu'une collection nommée Books existe déjà et qu'elle possède une partition nommée Novels.

    collection = Collection("Books")
    
    # Use the load method of a collection to load one of its partition
    collection.load(["Novels"], replica_number=2, _resource_groups=resource_groups)
    
    # Or, you can use the load method of a partition directly
    partition = Partition(collection, "Novels")
    partition.load(replica_number=2, _resource_groups=resource_groups)
    

    Notez que _resource_groups est un paramètre facultatif et que s'il n'est pas spécifié, Milvus chargera les répliques sur les nœuds de requête dans le groupe de ressources par défaut.

    Pour que Milus charge chaque réplique d'une collection dans un groupe de ressources distinct, assurez-vous que le nombre de groupes de ressources est égal au nombre de répliques.

  6. Transférer les répliques entre les groupes de ressources.

    Milvus utilise les répliques pour équilibrer la charge entre les segments distribués sur plusieurs nœuds de requête. Vous pouvez déplacer certaines répliques d'une collection d'un groupe de ressources à un autre de la manière suivante :

    source = '__default_resource_group'
    target = 'rg'
    collection_name = 'c'
    num_replicas = 1
    
    try:
        utility.transfer_replica(source, target, collection_name, num_replicas, using="default")
        print(f"Succeeded in moving {num_node} replica(s) of {collection_name} from {source} to {target}.")
    except Exception:
        print("Something went wrong while moving replicas.")
    
    # Succeeded in moving 1 replica(s) of c from __default_resource_group to rg.
    
  7. Abandonner un groupe de ressources.

    Vous pouvez à tout moment supprimer un groupe de ressources qui ne contient aucun nœud de requête (limits.node_num = 0). Dans ce guide, le groupe de ressources rg a maintenant un nœud de requête. Vous devez d'abord modifier la configuration limits.node_num du groupe de ressources pour la mettre à zéro.

    try:
        utility.update_resource_groups({
            "rg": utility.ResourceGroupConfig(
                requests={"node_num": 0},
                limits={"node_num": 0},
            ),
        }, using="default")
        utility.drop_resource_group("rg", using="default")
        print(f"Succeeded in dropping {source}.")
    except Exception:
        print(f"Something went wrong while dropping {source}.")
    

Pour plus de détails, veuillez vous référer aux exemples pertinents dans pymilvus.

Une bonne pratique pour gérer la mise à l'échelle des clusters

Actuellement, Milvus ne peut pas être mis à l'échelle de manière indépendante dans les environnements cloud-native. Toutefois, en utilisant l'API Declarative Resource Group conjointement avec l'orchestration de conteneurs, Milvus peut facilement isoler et gérer les ressources pour les QueryNodes. Voici une bonne pratique pour gérer les QueryNodes dans un environnement en nuage :

  1. Par défaut, Milvus crée un __default_resource_group. Ce groupe de ressources ne peut pas être supprimé et sert également de groupe de ressources de chargement par défaut pour toutes les collections et les QueryNodes redondants lui sont toujours affectés. Par conséquent, nous pouvons créer un groupe de ressources en attente pour contenir les ressources QueryNode inutilisées, empêchant ainsi les ressources QueryNode d'être occupées par le __default_resource_group.

    De plus, si nous appliquons strictement la contrainte sum(.requests.nodeNum) <= queryNodeNum, nous pouvons contrôler précisément l'affectation des QueryNodes dans le cluster. Supposons qu'il n'y ait actuellement qu'un seul QueryNode dans le cluster et initialisons le cluster. Voici un exemple de configuration :

    from pymilvus import utility
    from pymilvus.client.types import ResourceGroupConfig
    
    _PENDING_NODES_RESOURCE_GROUP="__pending_nodes"
    
    def init_cluster(node_num: int):
        print(f"Init cluster with {node_num} nodes, all nodes will be put in default resource group")
        # create a pending resource group, which can used to hold the pending nodes that do not hold any data.
        utility.create_resource_group(name=_PENDING_NODES_RESOURCE_GROUP, config=ResourceGroupConfig(
            requests={"node_num": 0}, # this resource group can hold 0 nodes, no data will be load on it.
            limits={"node_num": 10000}, # this resource group can hold at most 10000 nodes 
        ))
    
        # update default resource group, which can used to hold the nodes that all initial node in it.
        utility.update_resource_groups({
            "__default_resource_group": ResourceGroupConfig(
                requests={"node_num": node_num},
                limits={"node_num": node_num},
                transfer_from=[{"resource_group": _PENDING_NODES_RESOURCE_GROUP}], # recover missing node from pending resource group at high priority.
                transfer_to=[{"resource_group": _PENDING_NODES_RESOURCE_GROUP}], # recover redundant node to pending resource group at low priority.
            )})
        utility.create_resource_group(name="rg1", config=ResourceGroupConfig(
            requests={"node_num": 0},
            limits={"node_num": 0},
            transfer_from=[{"resource_group": _PENDING_NODES_RESOURCE_GROUP}], 
            transfer_to=[{"resource_group": _PENDING_NODES_RESOURCE_GROUP}],
        ))
        utility.create_resource_group(name="rg2", config=ResourceGroupConfig(
            requests={"node_num": 0},
            limits={"node_num": 0},
            transfer_from=[{"resource_group": _PENDING_NODES_RESOURCE_GROUP}], 
            transfer_to=[{"resource_group": _PENDING_NODES_RESOURCE_GROUP}],
        ))
    
    init_cluster(1)
    

    En utilisant l'exemple de code ci-dessus, nous créons un groupe de ressources nommé __pending_nodes pour contenir des QueryNodes supplémentaires. Nous créons également deux groupes de ressources spécifiques à l'utilisateur, nommés rg1 et rg2. De plus, nous nous assurons que l'autre groupe de ressources récupère en priorité les QueryNodes manquants ou redondants de __pending_nodes.

  2. Mise à l'échelle du cluster

    Supposons que nous disposions de la fonction de mise à l'échelle suivante :

    
    def scale_to(node_num: int):
        # scale the querynode number in Milvus into node_num.
        pass
    

    Nous pouvons utiliser l'API pour mettre à l'échelle un groupe de ressources spécifique jusqu'à un certain nombre de QueryNodes sans affecter les autres groupes de ressources.

    # scale rg1 into 3 nodes, rg2 into 1 nodes
    utility.update_resource_groups({
        "rg1": ResourceGroupConfig(
            requests={"node_num": 3},
            limits={"node_num": 3},
            transfer_from=[{"resource_group": _PENDING_NODES_RESOURCE_GROUP}],
            transfer_to=[{"resource_group": _PENDING_NODES_RESOURCE_GROUP}],
        ),
        "rg2": ResourceGroupConfig(
            requests={"node_num": 1},
            limits={"node_num": 1},
            transfer_from=[{"resource_group": _PENDING_NODES_RESOURCE_GROUP}],
            transfer_to=[{"resource_group": _PENDING_NODES_RESOURCE_GROUP}],
        ),
    })
    scale_to(5)
    # rg1 has 3 nodes, rg2 has 1 node, __default_resource_group has 1 node.
    
  3. Mise à l'échelle du cluster

    De même, nous pouvons établir des règles de mise à l'échelle qui donnent la priorité à la sélection des QueryNodes dans le groupe de ressources __pending_nodes. Ces informations peuvent être obtenues via l'API describe_resource_group. Atteindre l'objectif de mise à l'échelle du groupe de ressources spécifié.

    # scale rg1 from 3 nodes into 2 nodes
    utility.update_resource_groups({
        "rg1": ResourceGroupConfig(
            requests={"node_num": 2},
            limits={"node_num": 2},
            transfer_from=[{"resource_group": _PENDING_NODES_RESOURCE_GROUP}],
            transfer_to=[{"resource_group": _PENDING_NODES_RESOURCE_GROUP}],
        ),
    })
    
    # rg1 has 2 nodes, rg2 has 1 node, __default_resource_group has 1 node, __pending_nodes has 1 node.
    scale_to(4)
    # scale the node in __pending_nodes
    

Comment les groupes de ressources interagissent avec plusieurs répliques

  • Les répliques d'une collection unique et les groupes de ressources ont une relation N à N.
  • Lorsque plusieurs répliques d'une même collection sont chargées dans un groupe de ressources, les QueryNodes de ce groupe de ressources sont répartis de manière égale entre les répliques, de sorte que la différence entre le nombre de QueryNodes de chaque réplique ne dépasse pas 1.

Prochaines étapes

Pour déployer une instance Milvus multi-tenant, lisez ce qui suit :