Pregunta

Tengo una aplicación web que controla qué aplicaciones web reciben tráfico de nuestro equilibrador de carga. La aplicación web se ejecuta en cada servidor individual.

Realiza un seguimiento de " dentro o fuera " estado para cada aplicación en un objeto en el estado de aplicación ASP.NET, y el objeto se serializa en un archivo en el disco cada vez que se cambia el estado. El estado se deserializa del archivo cuando se inicia la aplicación web.

Si bien el sitio en sí solo recibe un par de solicitudes de un segundo como máximo, y el archivo al que rara vez accedía, descubrí que era extremadamente fácil por alguna razón tener colisiones al intentar leer o escribir en el archivo. Este mecanismo debe ser extremadamente confiable, porque tenemos un sistema automatizado que regularmente realiza implementaciones continuas en el servidor.

Antes de que alguien haga algún comentario cuestionando la prudencia de cualquiera de los anteriores, permítanme decir simplemente que explicar el razonamiento detrás de esto haría que esta publicación sea mucho más larga de lo que ya es, por lo que me gustaría evitar mover montañas.

Dicho esto, el código que uso para controlar el acceso al archivo tiene este aspecto:

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

Esto generalmente funciona, pero ocasionalmente no puedo acceder al mutex (veo el evento "No se pudo obtener un bloqueo" en el registro). No puedo reproducir esto localmente, solo ocurre en mis servidores de producción (Win Server 2k3 / IIS 6). Si elimino el tiempo de espera, la aplicación se bloquea indefinidamente (¿condición de carrera?), Incluso en solicitudes posteriores.

Cuando recibo los errores, al mirar el registro de eventos me dice que el bloqueo de mutex se logró y se liberó mediante la solicitud anterior antes de que se registrara el error.

El mutex se instancia en el evento Application_Start. Obtengo los mismos resultados cuando se crea una instancia estática en la declaración.

Excusas, excusas: enhebrar / bloquear no es mi fuerte & # 233 ;, ya que generalmente no tengo que preocuparme por eso.

¿Alguna sugerencia de por qué al azar no podría obtener una señal?


Update:

He agregado el manejo adecuado de errores (¡qué vergonzoso!), pero sigo recibiendo los mismos errores, y para el registro, las excepciones no controladas nunca fueron el problema.

Solo un proceso accedería al archivo: no uso un jardín web para el grupo web de esta aplicación, y ninguna otra aplicación usa el archivo. La única excepción que se me ocurre es cuando el grupo de aplicaciones se recicla, y el viejo WP todavía está abierto cuando se crea el nuevo, pero puedo observar al observar el administrador de tareas que el problema ocurre mientras solo hay un proceso de trabajo.

@mmr: ¿en qué se diferencia el uso de Monitor de Mutex? Según la documentación de MSDN, parece que efectivamente está haciendo lo mismo: si no puedo obtener el bloqueo con mi Mutex, falla falla simplemente devolviendo falso.

Otra cosa a tener en cuenta: los problemas que estoy teniendo parecen ser completamente aleatorios; si falla en una solicitud, podría funcionar bien en la siguiente. Tampoco parece haber un patrón (ciertamente no todos los demás, al menos).


Actualización 2:

Este bloqueo no se usa para ninguna otra llamada. El único momento en que se hace referencia a _lock fuera del método InvokeOnFile es cuando se instancia.

El Func que se invoca es leer del archivo y deserializarlo en un objeto, o serializar un objeto y escribirlo en el archivo. Ninguna operación se realiza en un hilo separado.

ServerState.PATH es un campo de solo lectura estático, que no creo que cause problemas de concurrencia.

También me gustaría repetir mi punto anterior de que no puedo reproducir esto localmente (en Cassini).


Lecciones aprendidas:

  • Utilice el manejo de errores adecuado (duh!)
  • Use la herramienta adecuada para el trabajo (y tenga una comprensión básica de qué / cómo funciona esa herramienta). Como
¿Fue útil?

Solución

Solo debe usar Mutexes si necesita sincronización entre procesos .

  

Aunque se puede usar un mutex para   sincronización de subprocesos dentro del proceso,   generalmente se prefiere usar Monitor,   porque los monitores fueron diseñados   específicamente para .NET Framework   y por lo tanto hacer un mejor uso de   recursos En contraste, el Mutex   clase es un contenedor para un Win32   construir. Si bien es más poderoso   que un monitor, requiere un mutex   transiciones de interoperabilidad que son más   computacionalmente caro que aquellos   requerido por la clase Monitor.

Si necesita admitir el bloqueo entre procesos, necesita un Mutex global .

El patrón que se utiliza es increíblemente frágil, no hay manejo de excepciones y no se asegura de que se libere su Mutex. Ese es un código realmente arriesgado y muy probablemente la razón por la que ves estos bloqueos cuando no hay tiempo de espera.

Además, si la operación de su archivo demora más de 1.5 segundos, existe la posibilidad de que Mutexes concurrentes no pueda capturarlo. Recomendaría obtener el bloqueo correcto y evitar el tiempo de espera.

Creo que es mejor reescribir esto para usar un candado. Además, parece que está llamando a otro método, si esto tarda una eternidad, el bloqueo se mantendrá para siempre. Eso es bastante arriesgado.

Esto es más corto y mucho más seguro:

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

Otros consejos

Si algunas de las operaciones de archivo fallan, el bloqueo no se liberará. Lo más probable es que ese sea el caso. Coloque las operaciones de archivo en el bloque try / catch y libere el bloqueo en el bloque finalmente.

De todos modos, si lee el archivo en su método Global.asax Application_Start, esto garantizará que nadie más esté trabajando en ello (usted dijo que el archivo se lee al iniciar la aplicación, ¿verdad?). Para evitar colisiones en el reajuste del grupo de aplicaciones, etc., puede intentar leer el archivo (suponiendo que la operación de escritura tome un bloqueo exclusivo), y luego esperar 1 segundo y volver a intentarlo si se produce una excepción.

Ahora, tiene el problema de sincronizar las escrituras. Cualquier método que decida cambiar el archivo debe tener cuidado de no invocar una operación de escritura si hay otra en curso con una simple declaración de bloqueo.

Veo un par de problemas potenciales aquí.

Editar para la actualización 2: si la función es una simple combinación de serialización / deserialización, las separaría en dos funciones diferentes, una en una función de 'serialización' y otra en una función de 'deserialización'. Realmente son dos tareas diferentes. Luego puede tener diferentes tareas específicas de bloqueo. Invoke es ingenioso, pero me he metido en muchos problemas por 'ingenioso' por 'trabajar'.

1) ¿Se está bloqueando su función LogInformation? Porque primero lo llamas dentro del mutex, y luego una vez que liberas el mutex. Entonces, si hay un bloqueo para escribir en el archivo / estructura de registro, entonces puede terminar con su condición de carrera allí. Para evitar eso, coloque el registro dentro de la cerradura.

2) Echa un vistazo usando la clase Monitor, que sé que funciona en C # y supongo que funciona en ASP.NET. Para eso, simplemente puede intentar obtener el bloqueo y, de lo contrario, fallar con gracia. Una forma de usar esto es seguir intentando obtener el bloqueo. (Edite por qué: consulte aquí ; básicamente , un mutex está en todos los procesos, el Monitor está en un solo proceso, pero fue diseñado para .NET y, por lo tanto, es preferido. Los documentos no ofrecen ninguna otra explicación real.

3) ¿Qué sucede si falla la apertura del flujo de archivos porque alguien más tiene el bloqueo? Eso generaría una excepción, y eso podría causar que este código se comporte mal (es decir, el bloqueo que aún tiene el hilo que tiene la excepción, y otro hilo puede acceder a él).

4) ¿Qué pasa con el func en sí? ¿Eso inicia otro hilo, o está completamente dentro del mismo hilo? ¿Qué pasa con el acceso a ServerState.PATH?

5) ¿Qué otras funciones pueden acceder a ServerState._lock? Prefiero que cada función que requiere un bloqueo obtenga su propio bloqueo, para evitar condiciones de carrera / punto muerto. Si tienes muchos hilos, y cada uno de ellos intenta bloquear el mismo objeto pero para tareas totalmente diferentes, entonces podrías terminar con puntos muertos y carreras sin ninguna razón realmente fácil de entender. He cambiado al código para reflejar esa idea, en lugar de usar un bloqueo global. (Me doy cuenta de que otras personas sugieren un bloqueo global; realmente no me gusta esa idea, debido a la posibilidad de que otras cosas la agarren para alguna tarea que no sea esta tarea).

    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;
}
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top