Вопрос

У меня есть веб-приложение, которое контролирует, какие веб-приложения получают обслуживаемый трафик от нашего балансировщика нагрузки.Веб-приложение выполняется на каждом отдельном сервере.

Он отслеживает состояние "включено или выключено" для каждого приложения в объекте в ASP.NET состояние приложения, и объект сериализуется в файл на диске при каждом изменении состояния.Состояние десериализуется из файла при запуске веб-приложения.

Хотя сам сайт получает не более пары запросов в секунду, а к файлу, к которому он обращался редко, я обнаружил, что по какой-то причине было чрезвычайно легко получить столкновения при попытке чтения из файла или записи в него.Этот механизм должен быть чрезвычайно надежным, потому что у нас есть автоматизированная система, которая регулярно выполняет постепенное развертывание на сервере.

Прежде чем кто-либо сделает какие-либо комментарии, ставящие под сомнение благоразумие любого из вышеперечисленных, позвольте мне просто сказать, что объяснение причин, стоящих за этим, сделало бы этот пост намного длиннее, чем он уже есть, поэтому я бы не хотел сворачивать горы.

Тем не менее, код, который я использую для управления доступом к файлу, выглядит следующим образом:

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

Это обычно работает, но иногда я не могу получить доступ к мьютексу (я вижу событие "Не удалось получить блокировку" в журнале).Я не могу воспроизвести это локально - это происходит только на моих производственных серверах (Win Server 2k3 / IIS 6).Если я удалю тайм-аут, приложение зависнет на неопределенный срок (состояние гонки??), в том числе при последующих запросах.

Когда я действительно получаю ошибки, просмотр журнала событий сообщает мне, что блокировка мьютекса была достигнута и снята предыдущим запросом до того , как ошибка была зарегистрирована.

Экземпляр мьютекса создается в событии Application_Start .Я получаю те же результаты, когда он создается статически в объявлении.

Оправдания, отговорки:нарезание потоков / блокировка - не мой конек, так как обычно мне не нужно беспокоиться об этом.

Есть какие-нибудь предложения относительно того, почему он случайным образом не смог бы получить сигнал?


Обновить:

Я добавил правильную обработку ошибок (как неловко!), Но я по-прежнему получаю те же ошибки - и, между прочим, необработанные исключения никогда не были проблемой.

Только один процесс мог бы получить доступ к файлу - я не использую web garden для веб-пула этого приложения, и никакие другие приложения не используют файл.Единственное исключение, о котором я могу подумать, это когда пул приложений перерабатывается, а старый WP все еще открыт при создании нового - но я могу сказать, наблюдая за диспетчером задач, что проблема возникает, когда существует только один рабочий процесс.

@mmr:Чем использование Monitor отличается от использования Мьютекса?Основываясь на документации MSDN, похоже, что он эффективно делает то же самое - если и я не могу получить блокировку с помощью моего Мьютекса, это делает завершите работу изящно, просто вернув false .

Еще одна вещь, на которую следует обратить внимание:Проблемы, с которыми я сталкиваюсь, кажутся совершенно случайными - если при выполнении одного запроса произойдет сбой, при следующем он может нормально работать.Похоже, здесь тоже нет какой-то закономерности (по крайней мере, во всех остальных).


Обновление 2:

Эта блокировка не используется ни для какого другого вызова.Единственный раз, когда на _lock ссылаются вне метода InvokeOnFile, - это когда он создается.

Вызываемая функция - это либо чтение из файла и десериализация в объект, либо сериализация объекта и запись его в файл.Ни одна из операций не выполняется в отдельном потоке.

ServerState.PATH - это статическое поле, доступное только для чтения, которое, я не ожидаю, вызовет какие-либо проблемы с параллелизмом.

Я также хотел бы повторить свое предыдущее замечание о том, что я не могу воспроизвести это локально (в Cassini).


Извлеченные уроки:

  • Используйте правильную обработку ошибок (да!)
  • Используйте подходящий инструмент для работы (и имейте базовое представление о том, что / как этот инструмент делает).Как указывает sambo, использование Мьютекса, по-видимому, сопряжено с большими накладными расходами, что вызывало проблемы в моем приложении, тогда как Monitor разработан специально для .NET.
Это было полезно?

Решение

Вы должны использовать мьютексы только в том случае, если вам нужно межпроцессная синхронизация.

Хотя мьютекс может использоваться для синхронизации потоков внутри процесса, обычно предпочтительнее использовать Monitor, поскольку мониторы были разработаны специально для .NET Framework и, следовательно, позволяют лучше использовать ресурсы.В отличие от этого, Мьютекс класс является оболочкой для Win32 конструкции.Несмотря на то, что он более мощный, чем monitor, мьютекс требует переходов взаимодействия, которые являются более дорогостоящими с точки зрения вычислений, чем те, которые требуются классом Monitor.

Если вам нужно поддерживать межпроцессную блокировку, вам нужен Глобальный мьютекс.

Используемый шаблон невероятно хрупок, обработка исключений отсутствует, и вы не гарантируете, что ваш Мьютекс будет освобожден.Это действительно рискованный код и, скорее всего, причина, по которой вы видите эти зависания, когда нет таймаута.

Кроме того, если ваша файловая операция когда-либо занимает больше 1,5 секунд, то есть вероятность, что параллельные мьютексы не смогут ее перехватить.Я бы рекомендовал правильно выполнить блокировку и избежать тайм-аута.

Я думаю, что лучше всего переписать это, чтобы использовать блокировку.Кроме того, похоже, что вы вызываете другой метод, если это займет вечность, блокировка будет удерживаться вечно.Это довольно рискованно.

Это и короче, и намного безопаснее:

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

Другие советы

Если какая-то из файловых операций завершится неудачей, блокировка не будет снята.Скорее всего, так оно и есть.Поместите файловые операции в блок try / catch и снимите блокировку в блоке finally.

В любом случае, если вы прочитаете файл в своем методе Global.asax Application_Start, это гарантирует, что никто другой над ним не работает (вы сказали, что файл считывается при запуске приложения, верно?).Чтобы избежать коллизий при перезапуске пула приложений и т.д., вы просто можете попытаться прочитать файл (предполагая, что операция записи требует исключительной блокировки), а затем подождать 1 секунду и повторить попытку, если возникнет исключение.

Теперь у вас возникла проблема с синхронизацией записей.Какой бы метод ни решил изменить файл, следует позаботиться о том, чтобы не вызывать операцию записи, если выполняется другая операция с помощью инструкции simple lock .

Я вижу здесь пару потенциальных проблем.

Редактировать для обновления 2:Если функция представляет собой простую комбинацию сериализации / десериализации, я бы разделил их на две разные функции, одну на функцию 'serialize', а другую на функцию 'deserialize'.Это действительно две разные задачи.Затем у вас могут быть разные задачи, зависящие от конкретной блокировки.Invoke - это здорово, но у меня самого было много неприятностей, когда я предпочитал "здорово", а не "работать".

1) Заблокирована ли ваша функция LogInformation?Потому что сначала вы вызываете его внутри мьютекса, а затем, как только вы освобождаете мьютекс.Таким образом, если есть блокировка для записи в файл журнала / структуру, то вы можете в конечном итоге получить там свое состояние гонки.Чтобы избежать этого, положите бревно внутрь замка.

2) Проверьте, используя класс Monitor, который, как я знаю, работает в C #, и я бы предположил, что работает в ASP.NET.Для этого вы можете просто попытаться получить блокировку и в противном случае изящно потерпеть неудачу.Один из способов использовать это - просто продолжать пытаться получить блокировку.(Отредактируйте, почему:видишь здесь;по сути, мьютекс используется во всех процессах, монитор находится только в одном процессе, но был разработан для .Предпочтительнее NET и so.Никаких других реальных объяснений в документах не дается.)

3) Что произойдет, если открытие filestream завершится неудачей, потому что у кого-то другого есть блокировка?Это вызвало бы исключение, и это могло бы привести к неправильному поведению этого кода (т. Е. блокировка все еще удерживается потоком, в котором есть исключение, и другой поток может получить к нему доступ).

4) Как насчет самой функции?Начинается ли это с другого потока, или это полностью в рамках одного потока?Как насчет доступа к ServerState.PATH сервера?

5) Какие другие функции могут получить доступ к ServerState._lock?Я предпочитаю, чтобы каждая функция, требующая блокировки, получала свою собственную блокировку, чтобы избежать условий гонки / взаимоблокировки.Если у вас много-много потоков, и каждый из них пытается заблокировать один и тот же объект, но для совершенно разных задач, то вы можете столкнуться с взаимоблокировками и гонками без какой-либо действительно понятной причины.Я изменил код, чтобы отразить эту идею, вместо того чтобы использовать какую-то глобальную блокировку.(Я понимаю, что другие люди предлагают глобальную блокировку;Мне действительно не нравится эта идея, из-за возможности того, что другие вещи используют ее для какой-то задачи, которая не является этой задачей).

    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;
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top