Тупик при доступе к StackExchange.Redis

Я захожу в тупиковую ситуацию при вызове StackExchange.Redis.

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


Если у вас тоже есть эта проблема и вы не хотите все это читать; я предлагаю вам попробовать установить PreserveAsyncOrder на false.

ConnectionMultiplexer connection = ...;
connection.PreserveAsyncOrder = false;

Это, вероятно, разрешит тупик, о котором идет речь в данном разделе вопросов и ответов, а также может улучшить производительность.


Наша установка

  • Код запускается либо как консольное приложение, либо как рабочая роль Azure.
  • Он предоставляет REST api с использованием HttpMessageHandler, поэтому точка входа является асинхронной.
  • Некоторые части кода имеют сходство с потоками (принадлежат и должны выполняться одним потоком).
  • Некоторые части кода являются асинхронными.
  • Мы выполняем синхронизацию через асинхронность < / em> и async- чрезмерная синхронизация антипаттернов. (смешивание await и _5 _ / _ 6_).
  • Мы используем только асинхронные методы при доступе к Redis.
  • Мы используем StackExchange.Redis 1.0.450 для .NET 4.5.

Тупик

Когда приложение / служба запускается, оно какое-то время работает нормально, затем внезапно (почти) все входящие запросы перестают работать, они никогда не дают ответа. Все эти запросы находятся в тупике, ожидая завершения вызова Redis.

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

Мы также выполняем вызовы Redis из фоновых потоков с низким приоритетом, и эти вызовы продолжают работать даже после возникновения взаимоблокировки.

Кажется, что взаимоблокировка возникнет только при вызове Redis в потоке пула потоков. Я больше не думаю, что это связано с тем, что эти вызовы выполняются на поток пула потоков. Скорее, похоже, что любой асинхронный вызов Redis без продолжения или с продолжением sync safe будет продолжать работать даже после возникновения тупиковой ситуации. (См. Что, по моему мнению, происходит ниже)

Связанный

Результаты отладки

Я обнаружил, что источник тупика, похоже, находится в ProcessAsyncCompletionQueue на строка 124 из CompletionManager.cs.

Фрагмент этого кода:

while (Interlocked.CompareExchange(ref activeAsyncWorkerThread, currentThread, 0) != 0)
{
    // if we don't win the lock, check whether there is still work; if there is we
    // need to retry to prevent a nasty race condition
    lock(asyncCompletionQueue)
    {
        if (asyncCompletionQueue.Count == 0) return; // another thread drained it; can exit
    }
    Thread.Sleep(1);
}

Я обнаружил это во время тупика; activeAsyncWorkerThread - это один из наших потоков, который ожидает завершения вызова Redis. (наш поток = поток пула потоков, выполняющий наш код). Таким образом, цикл выше считается продолжающимся вечно.

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

Интересно, связано ли это с проблемой перехвата потока (которую я не совсем понимаю)?

Что делать?

Два основных вопроса, которые я пытаюсь понять:

  1. Может ли смешивание await и _16 _ / _ 17_ быть причиной взаимоблокировок даже при работе без контекста синхронизации?

  2. Мы сталкиваемся с ошибкой / ограничением в StackExchange.Redis?

Возможное исправление?

Судя по результатам моей отладки, проблема в том, что:

next.TryComplete(true);

... в строке 162 дюйма CompletionManager.cs может при некоторых обстоятельствах позволить текущему потоку (который является активным асинхронным рабочим потоком) уйти и начать обработку другого кода, что может вызвать взаимоблокировку.

Не зная подробностей и просто думая об этом «факте», было бы логично временно освободить активный асинхронный рабочий поток во время TryComplete вызова.

Думаю, что-то вроде этого могло сработать:

// release the "active thread lock" while invoking the completion action
Interlocked.CompareExchange(ref activeAsyncWorkerThread, 0, currentThread);

try
{
    next.TryComplete(true);
    Interlocked.Increment(ref completedAsync);
}
finally
{
    // try to re-take the "active thread lock" again
    if (Interlocked.CompareExchange(ref activeAsyncWorkerThread, currentThread, 0) != 0)
    {
        break; // someone else took over
    }
}

Думаю, я больше всего надеюсь на то, что Марк Гравелл прочитает это и поделится своим мнением :-)

Нет контекста синхронизации = Контекст синхронизации по умолчанию

Выше я писал, что в нашем коде не используется контекст синхронизации. Это верно лишь частично: код запускается либо как консольное приложение, либо как рабочая роль Azure. В этих средах _22 _ равно null, поэтому я написал, что мы работаем без контекста синхронизации.

Однако после прочтения Все дело в контексте синхронизации я узнал, что это не совсем так:

По соглашению, если текущий SynchronizationContext потока имеет значение null, тогда он неявно имеет SynchronizationContext по умолчанию.

Однако контекст синхронизации по умолчанию не должен быть причиной взаимоблокировок, в отличие от контекста синхронизации на основе пользовательского интерфейса (WinForms, WPF), поскольку он не подразумевает сходство потоков.

Что я думаю происходит

Когда сообщение завершено, его источник завершения проверяется на предмет того, считается ли оно безопасным для синхронизации. Если это так, действие завершения выполняется встроенно, и все в порядке.

Если это не так, идея состоит в том, чтобы выполнить действие завершения для недавно выделенного потока пула потоков. Это тоже отлично работает, когда ConnectionMultiplexer.PreserveAsyncOrder равно false.

Однако, когда ConnectionMultiplexer.PreserveAsyncOrder равно true (значение по умолчанию), эти потоки пула потоков будут сериализовать свою работу, используя очередь завершения и гарантируя, что не более одного из них является активным асинхронным рабочим потоком. в любое время.

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

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

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

Предлагаемое мной «исправление» (см. Выше) не приведет к возникновению тупиковой ситуации таким образом, однако оно нарушит идею сохранения порядка асинхронного завершения.

Так что, возможно, здесь следует сделать вывод, что небезопасно смешивать await с _29 _ / _ 30_, когда PreserveAsyncOrder равно true, независимо от того, работаем ли мы без контекста синхронизации?

(По крайней мере, пока мы не сможем использовать .NET 4.6 и новый _ 33_, я полагаю)


person Mårten Wikström    schedule 12.06.2015    source источник
comment
Здесь очень сложно составить мнение, потому что вы не показываете какой-либо код, который на самом деле вызывает SE.Redis, или ожидает / ожидает - что является критическим кодом ... вы можете показать, что вызываете его?   -  person Marc Gravell    schedule 15.06.2015
comment
@MarcGravell: Я могу показать вам любой код, но не полностью. Однако проблема в том, что я не знаю, какой код здесь интереснее. Пожалуйста, посмотрите мое недавнее изменение (в конце), я думаю, что проблема является общей и связана с небезопасным для синхронизации действием завершения, выполняемым активным асинхронным рабочим потоком, который будет вызвать тупик при блокировке.   -  person Mårten Wikström    schedule 16.06.2015
comment
Хотя не ответ, что хорошо написанный вопрос.   -  person Nico    schedule 16.06.2015
comment
Асинхронная взаимоблокировка синхронизации также возникает в приложениях asp.net, когда контекст синхронизации, который вызывал метод async, является тем, к которому он пытается вернуться, даже если это происходит из фонового потока.   -  person eran otzap    schedule 24.06.2015
comment
Я вижу тот же сценарий в каком-то конкретном случае, воспроизводимый в моей локальной среде разработки. Не уверен, что вызывает это, но это тот же самый симптом тупика - qs говорит, что материал отправлен, in говорит, что материал получен, но он зависает. Это с полностью синхронизирующими вызовами SE Redis, без асинхронности. Настройка PreserveAsyncOrder исправляет это, но это кажется магическим. @MarcGravell есть идеи по этому поводу?   -  person Chris Hynes    schedule 05.03.2017
comment
Как определить тупик? В сообщении об ошибке упоминается тупик или клиент получает ошибку тайм-аута?   -  person Keith    schedule 24.05.2018
comment
@Keith, вы не получите сообщение об ошибке, когда возникнет тупик. Все пути кода, пытающиеся войти в критическую секцию, которая находится в тупике, просто никогда не завершатся. Если что-то перестает отвечать, казалось бы, навсегда, вы можете попасть в тупиковую ситуацию.   -  person Mårten Wikström    schedule 24.05.2018
comment
PreserveAsyncOrder больше не поддерживается (устарело), ​​интересно, это было исправлено в основной библиотеке?   -  person Matt Roberts    schedule 08.04.2020


Ответы (2)


Вот обходные пути, которые я нашел для этой проблемы с тупиком:

Обходной путь №1

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

Отключите это поведение, установив PreserveAsyncOrder в false.

ConnectionMultiplexer connection = ...;
connection.PreserveAsyncOrder = false;

Это позволит избежать взаимоблокировок, а также может повысить производительность.

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

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


Обходной путь №2

Тупиковая ситуация возникает, когда активный асинхронный рабочий поток в StackExchange.Redis завершает команду и когда задача завершения выполняется в оперативном режиме.

Можно предотвратить выполнение задачи в оперативном режиме, используя настраиваемый _ 4_ и убедитесь, что _ 5_ возвращает false.

public class MyScheduler : TaskScheduler
{
    public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
    {
        return false; // Never allow inlining.
    }

    // TODO: Rest of TaskScheduler implementation goes here...
}

Создание хорошего планировщика задач может оказаться сложной задачей. Однако существуют существующие реализации в библиотеке ParallelExtensionExtras (пакет NuGet), который можно использовать или вдохновить.

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

public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
    // Don't allow inlining on a thread pool thread.
    return !Thread.CurrentThread.IsThreadPoolThread && this.TryExecuteTask(task);
}

Еще одна идея - прикрепить планировщик ко всем его потокам, используя локальное хранилище потока.

private static ThreadLocal<TaskScheduler> __attachedScheduler 
                   = new ThreadLocal<TaskScheduler>();

Убедитесь, что это поле назначается при запуске потока и очищается по завершении:

private void ThreadProc()
{
    // Attach scheduler to thread
    __attachedScheduler.Value = this;

    try
    {
        // TODO: Actual thread proc goes here...
    }
    finally
    {
        // Detach scheduler from thread
        __attachedScheduler.Value = null;
    }
}

Затем вы можете разрешить встраивание задач, если это выполняется в потоке, который «принадлежит» настраиваемому планировщику:

public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
    // Allow inlining on our own threads.
    return __attachedScheduler.Value == this && this.TryExecuteTask(task);
}
person Mårten Wikström    schedule 18.06.2015
comment
Примечание. Начиная с версии 2.0.495 PreserveAsyncOrder устарела. - person tehmas; 21.02.2019
comment
@tehmas есть ли предложения относительно того, где новый флаг находится внутри ConnectionMultiplexer после того, как PreserveAsyncOrder устареет? Или если в StackExchange.Redis где-то еще есть флаг? - person chy600; 07.10.2020

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

Здесь подробно описаны ограничения на HTTP-запросы. Подобно старой проблеме WCF, когда вы не удалили соединение, а затем все соединения WCF завершились ошибкой.

Максимальное количество одновременных запросов HttpWebRequests

Это скорее помощь при отладке, поскольку я сомневаюсь, что вы действительно используете все TCP-порты, но это хорошая информация о том, как узнать, сколько у вас открытых портов и где.

https://msdn.microsoft.com/en-us/library/aa560610(v=bts.20).aspx

person Josh    schedule 15.06.2015
comment
Спасибо. Но эта проблема не вызвана исчерпанием портов TCP или HTTP-соединений. - person Mårten Wikström; 16.06.2015