Question

J'ai une application Web qui contrôle les applications Web qui reçoivent le trafic desservi par notre équilibreur de charge. L’application Web est exécutée sur chaque serveur individuel.

Il garde la trace du " in ou out " state pour chaque application dans un objet dans l'état ASP.NET, et l'objet est sérialisé dans un fichier sur le disque chaque fois que l'état est modifié. L'état est désérialisé du fichier au démarrage de l'application Web.

Bien que le site lui-même ne reçoive que quelques requêtes, ainsi que le fichier auquel il accédait rarement, j’ai trouvé qu’il était extrêmement facile, pour une raison quelconque, d’obtenir des collisions en tentant de lire ou d’écrire dans le fichier. Ce mécanisme doit être extrêmement fiable, car nous avons un système automatisé qui effectue régulièrement des déploiements successifs sur le serveur.

Avant que quiconque ne fasse quelque remarque que ce soit mettant en doute la prudence de tout ce qui précède, permettez-moi de dire simplement qu'expliquer le raisonnement derrière cela rendrait ce message beaucoup plus long qu'il ne l'est déjà, aussi j'aimerais éviter de déplacer des montagnes.

Cela dit, le code que j'utilise pour contrôler l'accès au fichier se présente comme suit:

internal static Mutex _lock = null;
/// <summary>Executes the specified <see cref="Func{FileStream, Object}" /> delegate on 
/// the filesystem copy of the <see cref="ServerState" />.
/// The work done on the file is wrapped in a lock statement to ensure there are no 
/// locking collisions caused by attempting to save and load the file simultaneously 
/// from separate requests.
/// </summary>
/// <param name="action">The logic to be executed on the 
/// <see cref="ServerState" /> file.</param>
/// <returns>An object containing any result data returned by <param name="func" />. 
///</returns>
private static Boolean InvokeOnFile(Func<FileStream, Object> func, out Object result)
{
    var l = new Logger();
    if (ServerState._lock.WaitOne(1500, false))
    {
        l.LogInformation( "Got lock to read/write file-based server state."
                        , (Int32)VipEvent.GotStateLock);
        var fileStream = File.Open( ServerState.PATH, FileMode.OpenOrCreate 
                                  , FileAccess.ReadWrite, FileShare.None);                
        result = func.Invoke(fileStream);                
        fileStream.Close();
        fileStream.Dispose();
        fileStream = null;
        ServerState._lock.ReleaseMutex();
        l.LogInformation( "Released state file lock."
                        , (Int32)VipEvent.ReleasedStateLock);
        return true;
    }
    else
    {
        l.LogWarning( "Could not get a lock to access the file-based server state."
                    , (Int32)VipEvent.CouldNotGetStateLock);
        result = null;
        return false;
    }
}

Cela habituellement fonctionne, mais parfois, je ne peux pas accéder au mutex (je vois l'événement "Impossible d'obtenir un verrou" dans le journal). Je ne peux pas reproduire cela localement - cela ne se produit que sur mes serveurs de production (Win Server 2k3 / IIS 6). Si je supprime le délai d'attente, l'application est suspendue indéfiniment (condition de concurrence?), Y compris lors des demandes suivantes.

Lorsque j'obtiens les erreurs, le journal des événements m'indique que le verrou mutex a été atteint et libéré par la requête précédente avant que l'erreur ne soit consignée.

Le mutex est instancié dans l'événement Application_Start. J'obtiens les mêmes résultats quand il est instancié statiquement dans la déclaration.

Excuses, excuses: le filetage / verrouillage n’est pas mon fort & # 233 ;, comme je n’ai généralement pas à m'en soucier.

Avez-vous des suggestions quant à la raison pour laquelle il ne pourrait pas recevoir de signal de manière aléatoire?

Mise à jour:

J'ai ajouté la gestion des erreurs appropriée (c'est embarrassant!), mais je reçois toujours les mêmes erreurs - et pour mémoire, les exceptions non gérées n'ont jamais posé problème.

Un seul processus accédera jamais au fichier - je n'utilise pas de jardin Web pour le pool Web de cette application, et aucune autre application n'utilise le fichier. La seule exception à laquelle je peux penser serait lorsque le pool d'applications sera recyclé, et l'ancien WP restera ouvert lorsque le nouveau sera créé - mais je peux dire en observant le gestionnaire de tâches que le problème se produit alors qu'il n'y a qu'un seul processus de travail.

@mmr: En quoi l'utilisation de Monitor est-elle différente de l'utilisation d'un mutex? D'après la documentation MSDN, il semblerait qu'il fasse effectivement la même chose - si et je ne peux pas obtenir le verrou avec mon Mutex, il échoue simplement en renvoyant la valeur false.

Autre chose à noter: les problèmes que je rencontre semblent être complètement aléatoires: si cela échoue avec une requête, cela pourrait fonctionner correctement pour la suivante. Il ne semble pas y avoir de tendance non plus (certainement pas toutes les autres, du moins).

Mise à jour 2:

Ce verrou n'est utilisé pour aucun autre appel. Le seul moment où _lock est référencé en dehors de la méthode InvokeOnFile est lorsqu’il est instancié.

Le Func appelé est soit la lecture du fichier et sa désérialisation dans un objet, soit la sérialisation d'un objet et son écriture dans le fichier. Aucune opération n’est effectuée sur un thread séparé.

ServerState.PATH est un champ statique en lecture seule qui, selon moi, ne causerait aucun problème de simultanéité.

Je voudrais également répéter ce que j'ai déjà dit, à savoir que je ne peux pas reproduire cela localement (en Cassini).

Leçons apprises:

  • Utilisez le traitement d'erreur approprié (duh!)
  • Utilisez le bon outil pour le travail (et comprenez ce que fait cet outil / comment il fonctionne). Comme
Était-ce utile?

La solution

Vous ne devez utiliser des mutex que si vous avez besoin de la synchronisation entre processus .

  

Bien qu'un mutex puisse être utilisé pour   synchronisation de threads intra-processus,   utiliser Monitor est généralement préféré,   parce que les moniteurs ont été conçus   spécifiquement pour le .NET Framework   et donc faire un meilleur usage de   Ressources. En revanche, le Mutex   la classe est un wrapper pour un Win32   construction. Alors que c'est plus puissant   qu'un moniteur, un mutex nécessite   les transitions interop qui sont plus   coûteux en calcul que ceux   requis par la classe Monitor.

Si vous devez prendre en charge le verrouillage interprocessus, vous devez disposer d'un mutex global .

Le modèle utilisé est incroyablement fragile, il n’ya pas de traitement des exceptions et vous ne vous assurez pas que votre Mutex est libéré. C’est un code vraiment risqué et très probablement la raison pour laquelle vous voyez ces problèmes s’arrêter quand il n’ya pas de timeout.

De plus, si votre opération de fichier dure plus de 1,5 seconde, il est possible que les mutex mutuels ne puissent pas le récupérer. Je recommanderais d'obtenir le bon verrouillage et d'éviter le délai d'attente.

Je pense qu'il est préférable de ré-écrire ceci pour utiliser un verrou. En outre, il semble que vous appeliez une autre méthode. Si cela prend une éternité, le verrou sera maintenu pour toujours. C'est assez risqué.

C’est à la fois plus court et beaucoup plus sûr:

// if you want timeout support use 
// try{var success=Monitor.TryEnter(m_syncObj, 2000);}
// finally{Monitor.Exit(m_syncObj)}
lock(m_syncObj)
{
    l.LogInformation( "Got lock to read/write file-based server state."
                    , (Int32)VipEvent.GotStateLock);
    using (var fileStream = File.Open( ServerState.PATH, FileMode.OpenOrCreate
                                     , FileAccess.ReadWrite, FileShare.None))
    {
        // the line below is risky, what will happen if the call to invoke
        // never returns? 
        result = func.Invoke(fileStream);
    }
}

l.LogInformation("Released state file lock.", (Int32)VipEvent.ReleasedStateLock);
return true;

// note exceptions may leak out of this method. either handle them here.
// or in the calling method. 
// For example the file access may fail of func.Invoke may fail

Autres conseils

Si certaines des opérations sur les fichiers échouent, le verrou ne sera pas libéré. Très probablement, c'est le cas. Placez les opérations sur les fichiers dans le bloc try / catch et libérez le verrou dans le dernier bloc.

Quoi qu'il en soit, si vous lisez le fichier dans votre méthode Global.asax Application_Start, cela garantira que personne d'autre ne travaille dessus (vous avez dit que le fichier est lu au démarrage de l'application, n'est-ce pas?). Pour éviter les collisions lors de la restauration du pool d'applications, etc., vous pouvez simplement essayer de lire le fichier (en supposant que l'opération d'écriture utilise un verrou exclusif), puis attendre 1 seconde et réessayer si une exception est levée.

Maintenant, vous avez le problème de la synchronisation des écritures. Quelle que soit la méthode qui décide de modifier le fichier, veillez à ne pas appeler une opération d'écriture si une autre est en cours avec une instruction de verrouillage simple.

Je vois quelques problèmes potentiels ici.

Éditer pour la mise à jour 2: si la fonction est une simple combinaison sérialiser / désérialiser, je séparerais les deux sorties en deux fonctions différentes, l’une en une fonction 'sérialiser' et l’autre en une fonction 'désérialiser'. Ce sont vraiment deux tâches différentes. Vous pouvez alors avoir différentes tâches spécifiques à un verrou. Invoke, c'est chouette, mais j'ai eu beaucoup de difficulté à me lancer moi-même pour "chouette" pour "travailler".

1) Votre fonction LogInformation est-elle verrouillée? Parce que vous l'appelez d'abord dans le mutex, puis une fois que vous libérez le mutex. Donc, s'il existe un verrou pour écrire dans le fichier journal / la structure, vous pouvez alors vous retrouver avec votre condition de concurrence. Pour éviter cela, placez le journal à l'intérieur du verrou.

2) Vérifiez à l'aide de la classe Monitor, qui, je le sais, fonctionne en C # et supposerait qu'elle fonctionne en ASP.NET. Pour cela, vous pouvez simplement essayer d'obtenir le verrou et échouer normalement. Une façon de l'utiliser est simplement de continuer à essayer d'obtenir le verrou. (Modifier pour pourquoi: voir ici ; en gros , un mutex est un processus à la fois, le moniteur n’est qu’un processus, mais il a été conçu pour .NET et est donc préférable. Aucune autre explication n’est donnée par la documentation.)

3) Que se passe-t-il si l'ouverture du flux de fichiers échoue, car quelqu'un d'autre a le verrou? Cela jetterait une exception et pourrait causer un mauvais comportement de ce code (c’est-à-dire que le verrou est toujours maintenu par le thread qui a l’exception et qu’un autre thread peut l’atteindre).

4) Qu'en est-il de la fonction elle-même? Est-ce que cela commence un autre thread, ou est-ce entièrement dans le seul thread? Qu'en est-il de l'accès à ServerState.PATH?

5) Quelles autres fonctions peuvent accéder à ServerState._lock? Je préfère que chaque fonction nécessitant un verrou reçoive son propre verrou pour éviter les conditions de course / impasse. Si vous avez beaucoup de threads et que chacun essaye de se verrouiller sur le même objet mais pour des tâches totalement différentes, alors vous pouvez vous retrouver avec des blocages et des courses sans aucune raison vraiment facile à comprendre. J'ai changé de code pour refléter cette idée, plutôt que d'utiliser un verrou global. (Je me rends compte que d'autres personnes suggèrent un verrou global; je n'aime vraiment pas cette idée, car il est possible que d'autres objets s'en emparent pour une tâche autre que cette tâche).

    Object MyLock = new Object();
    private static Boolean InvokeOnFile(Func<FileStream, Object> func, out Object result)
{
    var l = null;
    var filestream = null;
    Boolean success = false;
    if (Monitor.TryEnter(MyLock, 1500))
        try {
            l = new Logger();
            l.LogInformation("Got lock to read/write file-based server state.", (Int32)VipEvent.GotStateLock);
            using (fileStream = File.Open(ServerState.PATH, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)){                
                result = func.Invoke(fileStream); 
            }    //'using' means avoiding the dispose/close requirements
            success = true;
         }
         catch {//your filestream access failed

            l.LogInformation("File access failed.", (Int32)VipEvent.ReleasedStateLock);
         } finally {
            l.LogInformation("About to released state file lock.", (Int32)VipEvent.ReleasedStateLock);
            Monitor.Exit(MyLock);//gets you out of the lock you've got
        }
    } else {
         result = null;
         //l.LogWarning("Could not get a lock to access the file-based server state.", (Int32)VipEvent.CouldNotGetStateLock);//if the lock doesn't show in the log, then it wasn't gotten; again, if your logger is locking, then you could have some issues here
    }
  return Success;
}
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top