Domanda

Ho un'applicazione web che controlla quali applicazioni web ricevono traffico dal nostro bilanciamento del carico. L'applicazione Web viene eseguita su ogni singolo server.

Tiene traccia di " in o out " stato per ogni applicazione in un oggetto nello stato dell'applicazione ASP.NET e l'oggetto viene serializzato su un file sul disco ogni volta che lo stato viene modificato. Lo stato viene deserializzato dal file all'avvio dell'applicazione Web.

Mentre il sito stesso riceve solo un paio di richieste al secondo, e il file a cui accede raramente, ho scoperto che è stato estremamente facile per qualche motivo ottenere collisioni durante il tentativo di leggere o scrivere sul file. Questo meccanismo deve essere estremamente affidabile, poiché disponiamo di un sistema automatizzato che esegue regolarmente il roll-out delle distribuzioni sul server.

Prima che qualcuno faccia commenti mettendo in dubbio la prudenza di quanto sopra, permettetemi di dire semplicemente che spiegare il ragionamento alla base renderebbe questo post molto più lungo di quanto non lo sia già, quindi vorrei evitare di spostare le montagne.

Detto questo, il codice che utilizzo per controllare l'accesso al file è simile al seguente:

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;
    }
}

Questo di solito funziona, ma a volte non riesco ad accedere al mutex (vedo l'evento "Impossibile ottenere un blocco" nel registro). Non riesco a riprodurlo localmente - succede solo sui miei server di produzione (Win Server 2k3 / IIS 6). Se rimuovo il timeout, l'applicazione si blocca indefinitamente (condizioni di gara ??), anche su richieste successive.

Quando ricevo gli errori, guardando il registro eventi mi viene comunicato che il blocco mutex è stato raggiunto e rilasciato dalla precedente richiesta prima dell'errore registrato.

Il mutex è istanziato nell'evento Application_Start. Ottengo gli stessi risultati quando viene istanziato staticamente nella dichiarazione.

Scuse, scuse: infilare / bloccare non è il mio forte & # 233 ;, in quanto generalmente non devo preoccuparmene.

Qualche suggerimento sul motivo per cui non riuscirebbe a ottenere un segnale a caso?


Aggiornamento:

Ho aggiunto la corretta gestione degli errori (che imbarazzo!), ma sto ancora ottenendo gli stessi errori - e per la cronaca, le eccezioni non gestite non sono mai state il problema.

Un solo processo accederà mai al file: non uso un web garden per il pool web di questa applicazione e nessun'altra applicazione usa il file. L'unica eccezione che mi viene in mente sarebbe quando il pool di app ricicla e il vecchio WP è ancora aperto quando viene creato quello nuovo, ma posso vedere guardando il task manager che il problema si verifica mentre c'è un solo processo di lavoro.

@mmr: In che modo l'utilizzo di Monitor è diverso dall'uso di un Mutex? Basato sulla documentazione MSDN, sembra che stia effettivamente facendo la stessa cosa - se e non riesco a ottenere il blocco con il mio Mutex, non fallisce con grazia restituendo semplicemente falso.

Un'altra cosa da notare: i problemi che sto riscontrando sembrano essere completamente casuali: se fallisce su una richiesta, potrebbe funzionare bene su quella successiva. Nemmeno sembra esserci uno schema (certamente non tutti gli altri, almeno).


Aggiornamento 2:

Questo blocco non viene utilizzato per nessun'altra chiamata. L'unica volta in cui viene fatto riferimento a _lock al di fuori del metodo InvokeOnFile è quando viene istanziato.

Il Func che viene invocato sta leggendo dal file e deserializzando in un oggetto, oppure serializzando un oggetto e scrivendolo nel file. Nessuna operazione viene eseguita su un thread separato.

ServerState.PATH è un campo di sola lettura statico, che non mi aspetto causerebbe problemi di concorrenza.

Vorrei anche reiterare il mio precedente punto che non riesco a riprodurre localmente (in Cassini).


Lezioni apprese:

  • Utilizza la corretta gestione degli errori (duh!)
  • Utilizzare lo strumento giusto per il lavoro (e avere una conoscenza di base di cosa / come fa quello strumento). Come
È stato utile?

Soluzione

Dovresti usare i mutex solo se hai bisogno di sincronizzazione tra processi .

  

Sebbene sia possibile utilizzare un mutex   sincronizzazione dei thread all'interno del processo,   l'utilizzo di Monitor è generalmente preferito,   perché sono stati progettati i monitor   specificamente per .NET Framework   e quindi fare un uso migliore di   risorse. Al contrario, il Mutex   class è un wrapper per un Win32   costruire. Mentre è più potente   di un monitor, richiede un mutex   interazioni transizioni che sono di più   computazionalmente costoso di quelli   richiesto dalla classe Monitor.

Se è necessario supportare il blocco tra processi è necessario un Mutex globale .

Il modello in uso è incredibilmente fragile, non ci sono eccezioni nella gestione e non stai assicurando che il tuo Mutex sia rilasciato. Questo è un codice davvero rischioso e molto probabilmente il motivo per cui li vedi si blocca quando non c'è timeout.

Inoltre, se l'operazione di file impiega mai più di 1,5 secondi, è possibile che i mutex simultanei non siano in grado di afferrarla. Consiglierei di ottenere il blocco corretto ed evitare il timeout.

Penso che sia meglio riscriverlo per usare un lucchetto. Inoltre, sembra che tu stia chiamando a un altro metodo, se questo richiede un'eternità, il blocco verrà mantenuto per sempre. È piuttosto rischioso.

Questo è sia più breve che molto più sicuro:

// 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

Altri suggerimenti

Se alcune operazioni sui file falliscono, il blocco non verrà rilasciato. Molto probabilmente è così. Inserisci le operazioni sui file nel blocco try / catch e rilascia il blocco nel blocco finally.

Ad ogni modo, se leggi il file nel tuo metodo Global.asax Application_Start, questo assicurerà che nessun altro ci stia lavorando (hai detto che il file viene letto all'avvio dell'applicazione, giusto?). Per evitare collisioni sul riavvio del pool di applicazioni, ecc., Puoi semplicemente provare a leggere il file (supponendo che l'operazione di scrittura richieda un blocco esclusivo), quindi attendi 1 secondo e riprova se viene generata un'eccezione.

Ora hai il problema di sincronizzare le scritture. Qualunque metodo decida di modificare il file, fare attenzione a non invocare un'operazione di scrittura se è in corso un'altra operazione con una semplice istruzione di blocco.

Vedo un paio di potenziali problemi qui.

Modifica per l'aggiornamento 2: se la funzione è una semplice combinazione di serializzazione / deserializzazione, separerei le due in due diverse funzioni, una in una funzione di "serializzazione" e una in una funzione di "deserializzazione". Sono davvero due compiti diversi. È quindi possibile svolgere diverse attività specifiche del blocco. Invoke è ingegnoso, ma io stesso ho avuto molti guai nel cercare "astuto" per "lavorare".

1) La funzione LogInformation è bloccata? Perché lo chiami prima nel mutex e poi quando rilasci il mutex. Quindi, se c'è un lucchetto da scrivere nel file / struttura di registro, allora puoi finire con le tue condizioni di gara. Per evitarlo, metti il ??registro nel lucchetto.

2) Scopri usando la classe Monitor, che so funziona in C # e suppongo che funzioni in ASP.NET. Per questo, puoi semplicemente provare a ottenere il blocco e fallire con grazia altrimenti. Un modo per usarlo è semplicemente continuare a cercare di ottenere il blocco. (Modifica per il motivo: vedi qui ; sostanzialmente , un mutex è attraverso i processi, il Monitor è in un solo processo, ma è stato progettato per .NET e quindi è preferito. Nessun'altra vera spiegazione è data dai documenti.)

3) Cosa succede se l'apertura del filestream fallisce, perché qualcun altro ha il lucchetto? Ciò genererebbe un'eccezione e potrebbe comportare un comportamento errato di questo codice (vale a dire, il blocco è ancora trattenuto dal thread che ha l'eccezione e un altro thread può ottenerlo).

4) E la funzione stessa? Questo avvia un altro thread o è interamente all'interno di un thread? Che dire dell'accesso a ServerState.PATH?

5) Quali altre funzioni possono accedere a ServerState._lock? Preferisco avere ogni funzione che richiede un blocco ottenere il proprio blocco, per evitare condizioni di gara / deadlock. Se hai molti thread e ognuno di essi prova a bloccare lo stesso oggetto ma per compiti totalmente diversi, potresti finire con deadlock e gare senza alcun motivo facilmente comprensibile. Ho cambiato il codice per riflettere quell'idea, piuttosto che usare un blocco globale. (Mi rendo conto che altre persone suggeriscono un blocco globale; non mi piace davvero quell'idea, a causa della possibilità che altre cose lo afferrino per un compito che non è questo compito).

    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;
}
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top