Recherche d'un schéma de verrouillage distribué

10

J'ai besoin de trouver un mécanisme \ pattern de verrouillage d'objet récursif personnalisé pour un système distribué en C #. Essentiellement, j'ai un système multi-nœuds. Chaque nœud dispose d' autorisations d' écriture exclusives sur n éléments d'état. Le même état est également disponible en lecture seule sur au moins un autre nœud. Certaines écritures / mises à jour doivent être atomiques sur tous les nœuds, tandis que d'autres mises à jour finiront par devenir cohérentes via des processus de réplication en arrière-plan, des files d'attente, etc.

Pour les mises à jour atomiques, je recherche un modèle ou des échantillons qui me permettent efficacement de marquer un objet comme verrouillé pour les écritures que je peux ensuite distribuer, valider, restaurer, etc ... Puisque le système a des niveaux de concurrence élevés, Je suppose que je devrai pouvoir empiler des verrous qui expireront ou seront déroulés une fois les verrous libérés.

Les éléments de transaction ou de messagerie ne sont pas au centre de cette question, mais je les ai fournis pour un contexte supplémentaire. Cela dit, n'hésitez pas à exprimer les messages qui, selon vous, seraient nécessaires si vous le souhaitez.

Voici un vague échantillon de ce que j'envisageais bien que je sois ouvert à de nouvelles idées en dehors de la mise en œuvre de nouveaux produits

thing.AquireLock(LockLevel.Write);

//Do work

thing.ReleaseLock();

Je pensais utiliser des méthodes d'extension, qui pourraient ressembler à ceci

public static void AquireLock(this IThing instance, TupleLockLevel lockLevel)
{ 
    //TODO: Add aquisition wait, retry, recursion count, timeout support, etc...  
    //TODO: Disallow read lock requests if the 'thing' is already write locked
    //TODO: Throw exception when aquisition fails
    instance.Lock = lockLevel;
}

public static void ReleaseLock(this IThing instance)
{
    instance.Lock = TupleLockLevel.None;
}

Pour clarifier quelques détails ...

  • Toutes les communications sont TCP / IP utilisant un protocole de requête / réponse binaire
  • Il n'y a pas de technologies intermédiaires telles que les files d'attente ou les bases de données
  • Il n'y a pas de nœud maître central. Dans ce cas, la disposition de verrouillage est définie par l'initiateur de la serrure et le partenaire qui honorera la demande avec une certaine forme de délai pour régir son comportement

Quelqu'un a des suggestions?

JoeGeeky
la source
Les verrous sont généralement une caractéristique standard dans la plupart des systèmes. Je suppose que c'est également le cas pour C #. (Un résultat de recherche Google: albahari.com/threading/part2.aspx ) Essayez-vous de réaliser quelque chose au-delà du Mutex de base ou des sémaphores?
Dipan Mehta
2
@DipanMehta Désolé, j'aurais dû aborder cela plus clairement. Les nœuds que j'ai mentionnés sont des machines sur un réseau. Ma compréhension de Mutex et des sémaphores est que ce sont des verrous à l'échelle de la machine ( par exemple, cross-process ) et non des verrous qui peuvent s'étendre entre des machines sur un réseau.
JoeGeeky
@JoeGeeky Votre question est sur le sujet ici et serait peut-être trop théorique pour Stack Overflow . Si vous voulez le demander à nouveau, vous pouvez, mais vous voudrez un phrasé plus axé sur le code.
Adam Lear

Réponses:

4

Merci pour les clarifications.

Dans ce cas, je recommanderais d'utiliser un modèle de publication / abonnement. Protocole de verrouillage distribué Chubby de Google (une implémentation de Paxos )

Je n'ai jamais utilisé Paxos (ou Chubby), mais il semble y avoir une implémentation open source ici .

Si cela ne fonctionne pas, vous pouvez implémenter votre propre version de Paxos en utilisant, par exemple, l'un des suspects habituels en termes de bibliothèques de messagerie: la bibliothèque de file d'attente de messages zéro , RabbitMQ ou ActiveMQ .


Réponse précédente:

La plupart des suggestions sur SO ( [A] , [B] ) vont pour l'utilisation d'une file d'attente de messages pour réaliser le verrouillage entre machines.

Votre AcquireLockméthode pousserait quelque chose identifiant l'objet verrou dans la file d'attente, vérifiant les instances précédentes de verrous avant de réussir. Votre ReleaseLockméthode supprimerait l'objet verrou de la file d'attente.

L'utilisateur atlantis de SO suggère, dans ce post , le post de Jeff Key pour certains détails.

Peter K.
la source
Merci, mais ces solutions ne conviendraient pas car je n'ai pas de maître central, de base de données ou de file d'attente. J'ai mis à jour la question avec quelques détails supplémentaires pour clarifier certains de ces détails.
JoeGeeky
Je ne pourrai pas utiliser ces produits directement car il existe déjà un protocole bien défini que je dois utiliser pour toutes les communications entre les nœuds, mais le Chubby et le Paxos peuvent avoir des modèles bien définis que je peux apprendre. Je regarderai.
JoeGeeky
@JoeGeeky Oui, le lien Paxos a des diagrammes de séquence qui pourraient vous permettre de l'implémenter en utilisant votre lien de communication préféré.
Peter K.
Bien que ce ne soit pas une réponse directe, la lecture de toutes les choses Chubby et Paxos m'a aidé à définir ma propre solution. Je n'ai pas utilisé ces outils, mais j'ai pu définir un modèle raisonnable basé sur certains de leurs concepts. Merci.
JoeGeeky
@JoeGeeky: Heureux d'entendre que cela m'a aidé, au moins. Merci pour la tique.
Peter K.
4

Il me semble que vous avez ici quelques technologies mixtes:

  • communications (sur lesquelles vous comptez essentiellement comme étant 100% fiables ... ce qui peut être fatal)

  • verrouillage / exclusion mutuelle

  • délais d'attente (dans quel but)?

Un mot d'avertissement: les délais d'attente dans les systèmes distribués peuvent être lourds de dangers et de difficultés. S'ils sont utilisés, ils doivent être définis et utilisés très soigneusement car l'utilisation aveugle des délais d'attente ne résout pas un problème, elle reporte simplement la catastrophe. (Si vous voulez voir comment les délais d'attente doivent être utilisés, lisez et comprenez la documentation du protocole de communication HDLC. C'est un bon exemple d'utilisation appropriée et intelligente, en combinaison avec un système de codage de bits intelligent pour permettre la détection de choses comme la ligne IDLE) .

Pendant un certain temps, j'ai travaillé dans des systèmes distribués multiprocesseurs connectés via des liaisons de communication (pas TCP, autre chose). L'une des choses que j'ai apprises, c'est qu'en général, il y a des endroits dangereux pour la multi-programmation:

  • la dépendance aux files d'attente se termine généralement en larmes (si la file d'attente se remplit, vous avez des problèmes. À MOINS que vous puissiez calculer une taille de file d'attente qui ne se remplira jamais, auquel cas vous pourriez probablement utiliser une solution sans file d'attente)

  • la dépendance au verrouillage est douloureuse, essayez de penser s'il existe un autre moyen (si vous devez utiliser le verrouillage, regardez la littérature, le verrouillage distribué multiprocesseur a fait l'objet de nombreux articles acédémiques des 2-3 dernières décennies)

Si vous devez continuer à utiliser le verrouillage, alors:

Je suppose que vous n'utiliserez les délais d'attente que comme moyen de récupération de dernier recours, c'est-à-dire pour détecter une défaillance du système de communication sous-jacent. Je suppose en outre que votre système de communication TCP / IP a une bande passante élevée et peut être considéré comme une faible latence (idéalement zéro, mais cela ne se produit jamais).

Ce que je suggère, c'est que chaque nœud possède une liste de connectivité d'autres nœuds auxquels il peut se connecter. (Les nœuds ne se soucient pas d'où vient une connexion.) La population des tables auxquelles les nœuds peuvent se connecter est laissée comme une chose distincte à trier, vous n'avez pas dit si cela serait défini statiquement ou non. Des éléments tels que l'attribution des numéros de port IP où les connexions entreraient dans un nœud sont également ignorés de manière pratique - il peut y avoir de bonnes raisons d'accepter des demandes sur un seul port ou sur plusieurs ports. Cela doit être soigneusement examiné. Les facteurs incluront la mise en file d'attente implicite, la commande, l'utilisation des ressources, le type de système d'exploitation et les capacités.

Une fois que les nœuds savent à qui ils se connectent, ils peuvent envoyer à ce nœud une demande de verrouillage et doivent recevoir une réponse de verrouillage de ce nœud distant. Vous pouvez regrouper ces deux opérations dans un wrapper pour lui donner un aspect atomique. Cela a pour effet que les nœuds souhaitant acquérir un verrou feront un appel quelque chose comme:

if (get_lock(remote_node) == timeout) then
  {
    take some failure action - the comms network is down
  }

/* Lock is now acquired - do work here */

if (release_lock(remote_node) == timeout) then
  {
    take some failure action - the comms network is down
  }

les appels get_lock et release_lock devraient être quelque chose comme (en principe):

send_to_remote_node(lock_request)
get_from_remote_node_or_timeout(lock_reply, time)
if (result was timeout) then
  return timeout
else
  return ok

Avec un système de verrouillage distribué, vous devrez faire très attention que les unités de travail effectuées pendant le verrouillage sont petites et rapides car vous aurez de nombreux nœuds distants potentiellement bloqués en attente pour obtenir un verrou. Il s'agit en fait d'un système multiprocesseur / communication d'arrêt et d'attente qui est robuste mais qui n'a pas les meilleures performances possibles.

Une suggestion est d'adopter une approche complètement différente. Pouvez-vous utiliser un appel de procédure à distance où chaque appel RPC transporte un ensemble d'informations qui peuvent être traitées par le destinataire et qui supprime les besoins de verrous?


En relisant la question, il semble que vous ne vouliez pas vraiment vous soucier du côté communication des choses, vous voulez juste résoudre votre problème de verrouillage.

Ma réponse peut donc sembler un peu hors sujet, cependant, je pense que vous ne pouvez pas résoudre votre problème de verrouillage sans obtenir les bonnes pièces en dessous. Analogie: Construire une maison sur de mauvaises fondations la fait tomber ... Finalement.

vite_maintenant
la source
1
La sémantique des délais d'attente est en grande partie là pour traiter les nœuds qui disparaissent du réseau, ou pour traiter les gros retards dans les piles de verrouillage ... Cela limitera le temps passé bloqué en attendant d'acquérir un verrou et fournira à ceux qui le demandent une opportunité pour lancer d'autres processus au milieu de retards, d'échecs, etc. inattendus. De plus, cela empêcherait que quelque chose soit verrouillé pour toujours en cas de défaillance. J'apprécie vos préoccupations bien qu'à ce stade, je ne vois aucune alternative étant donné que finalement quelque chose échouera
JoeGeeky
Pour parler de certains de vos autres commentaires, je n'utilise pas les files d'attente en soi (dans le sens de la communication asynchrone), même si je m'attends à ce que les verrous soient empilés et libérés en utilisant un modèle FIFO. Je n'ai pas tout à fait réconcilié comment cela fonctionnera en termes de modèle de demande / réponse requis, à part cela, il devra bloquer d'une manière ou d'une autre et faire partie d'une plus grande poignée de main. Pour le moment, je travaille sur le mécanisme de verrouillage empilé dans un seul nœud, puis sur la façon dont il fonctionnera dans le scénario distribué. Je ferai un peu plus de lecture comme vous l'avez suggéré. Merci
JoeGeeky
@JoeGeeky - un FIFO est une file d'attente. Méfiez-vous des files d'attente. Réfléchissez bien à ce côté. Cela ressemble beaucoup à ce que vous n'allez pas simplement obtenir quelque chose "sur l'étagère", mais que vous devrez réfléchir soigneusement à votre problème et à sa solution.
quick_now
Je comprends ... J'essayais de clarifier la différence entre une file d'attente FIFO utilisée dans les processus asynchrones ( par exemple, un processus en file d'attente puis un autre retrait de file d'attente ). Dans ce cas, les choses devront être gérées dans l'ordre, mais le processus entrant dans la file d'attente ne partira pas jusqu'à ce que (a) ils obtiennent le verrou, (b) se voient refuser un verrou, ou (c) ils expirent et quittent la ligne. Plutôt faire la queue au guichet automatique. Cela se comporte comme un modèle FIFO dans le cas de réussite, mais les processus peuvent rester en panne avant d'atteindre le début de la ligne. Quant à l'étagère? Non, mais ce n'est pas un nouveau problème
JoeGeeky
0

Votre question peut être facilement mise en œuvre à l'aide d'un cache distribué comme NCache. Ce dont vous avez besoin est un mécanisme de verrouillage pessimiste où vous pouvez acquérir un verrou à l'aide d'un objet. Ensuite, effectuez vos tâches et opérations et libérez le verrou pour que d'autres applications soient utilisées ultérieurement.

Jetez un œil au code suivant;

Ici, vous acquérez un verrou sur une clé spécifique, puis effectuez des tâches (allant d'une ou plusieurs opérations), puis libérez enfin le verrou lorsque vous avez terminé.

// Instance of the object used to lock and unlock cache items in NCache
LockHandle lockHandle = new LockHandle();

// Specify time span of 10 sec for which the item remains locked
// NCache will auto release the lock after 10 seconds.
TimeSpan lockSpan = new TimeSpan(0, 0, 10); 

try
{
    // If item fetch is successful, lockHandle object will be populated
    // The lockHandle object will be used to unlock the cache item
    // acquireLock should be true if you want to acquire to the lock.
    // If item does not exists, account will be null
    BankAccount account = cache.Get(key, lockSpan, 
    ref lockHandle, acquireLock) as BankAccount;
    // Lock acquired otherwise it will throw LockingException exception

    if(account != null && account.IsActive)
    {
        // Withdraw money or Deposit
        account.Balance += withdrawAmount;
        // account.Balance -= depositAmount;

        // Insert the data in the cache and release the lock simultaneously 
        // LockHandle initially used to lock the item must be provided
        // releaseLock should be true to release the lock, otherwise false
        cache.Insert("Key", account, lockHandle, releaseLock); 
        //For your case you should use cache.Unlock("Key", lockHandle);
    }
    else
    {
        // Either does not exist or unable to cast
        // Explicitly release the lock in case of errors
        cache.Unlock("Key", lockHandle);
    } 
}
catch(LockingException lockException)
{
    // Lock couldn't be acquired
    // Wait and try again
}

Tiré du lien: http://blogs.alachisoft.com/ncache/distributed-locking/

Basit Anwer
la source