it-swarm.com.de

Deadlock beim Zugriff auf StackExchange.Redis

Beim Aufrufen von StackExchange.Redis ist ein Deadlock aufgetreten.

Ich weiß nicht genau, was los ist, was sehr frustrierend ist, und ich würde mich über jede Eingabe freuen, die helfen könnte, dieses Problem zu lösen oder zu umgehen.


Falls Sie auch dieses Problem haben und nicht alles lesen möchten; Ich schlage vor, dass Sie versuchen, PreserveAsyncOrder auf zu setzen false.

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

Dies wird wahrscheinlich die Art von Deadlock beheben, um die es in diesem Q & A geht, und könnte auch die Leistung verbessern.


Unser Setup

  • Der Code wird entweder als Konsolenanwendung oder als Azure Worker-Rolle ausgeführt.
  • Es macht eine REST api mit HttpMessageHandler verfügbar, sodass der Einstiegspunkt asynchron ist.
  • Einige Teile des Codes haben Thread-Affinität (gehört einem einzelnen Thread und muss von diesem ausgeführt werden).
  • Einige Teile des Codes sind nur asynchron.
  • Wir machen die sync-over-async und async-over-sync Anti-Muster. (Mischen von await und Wait()/Result).
  • Wir verwenden nur asynchrone Methoden, wenn wir auf Redis zugreifen.
  • Wir verwenden StackExchange.Redis 1.0.450 für .NET 4.5.

Sackgasse

Wenn die Anwendung/der Dienst gestartet wird, wird sie/er eine Weile normal ausgeführt, dann funktionieren plötzlich (fast) alle eingehenden Anforderungen nicht mehr und sie geben keine Antwort mehr. Alle diese Anforderungen sind blockiert und warten auf den Abschluss eines Anrufs bei Redis.

Interessanterweise bleibt jeder Aufruf von Redis nach dem Auftreten des Deadlocks hängen, jedoch nur dann, wenn diese Aufrufe von einer eingehenden API-Anforderung stammen, die im Thread-Pool ausgeführt wird.

Wir rufen auch Redis von Hintergrundthreads mit niedriger Priorität auf, und diese Anrufe funktionieren auch nach dem Auftreten des Deadlocks weiter.

Es scheint, als würde ein Deadlock nur beim Aufruf von Redis in einem Threadpool-Thread auftreten. Ich glaube nicht mehr, dass dies an der Tatsache liegt, dass diese Aufrufe auf einen Thread-Pool-Thread erfolgen. Vielmehr scheint es, als würde jeder asynchrone Redis-Aufruf ohne Fortsetzung oder mit einer sync safe -Fortsetzung auch nachher weiter funktionieren Die Deadlock-Situation ist aufgetreten. (Siehe Was ich denke passiert ​​unten)

Verbunden

  • StackExchange.Redis Deadlocking

    Deadlock durch Mischen von await und Task.Result (Sync-over-Async, wie wir es tun). Unser Code wird jedoch ohne Synchronisationskontext ausgeführt, sodass er hier nicht zutrifft, oder?

  • Wie kann man Synchronisierungs- und Asynchronisierungscode sicher mischen?

    Ja, das sollten wir nicht tun. Aber wir tun es, und wir müssen es noch eine Weile tun. Viel Code, der in die asynchrone Welt migriert werden muss.

    Auch hier haben wir keinen Synchronisationskontext, das sollte also keine Deadlocks verursachen, oder?

    Das Setzen von ConfigureAwait(false) vor einem await hat keine Auswirkung darauf.

  • Timeout-Ausnahme nach asynchronen Befehlen und Task.WhenAny wartet in StackExchange.Redis

    Dies ist das Thread-Hijacking-Problem. Wie ist die aktuelle Situation dazu? Könnte das hier das Problem sein?

  • Der asynchrone Aufruf von StackExchange.Redis hängt

    Aus Marc's Antwort:

    ... Mischen Warten und abwarten ist keine gute Idee. Zusätzlich zu Deadlocks ist dies "Sync over Async" - ein Anti-Pattern.

    Er sagt aber auch:

    SE.Redis umgeht den Sync-Kontext intern (normal für Bibliothekscode), so dass es keinen Deadlock geben sollte

    Nach meinem Verständnis sollte StackExchange.Redis daher unabhängig davon sein, ob wir das Antimuster Sync-over-Async verwenden. Es wird nur nicht empfohlen, da dies die Ursache für Deadlocks in anderem Code sein kann.

    In diesem Fall befindet sich der Deadlock jedoch, soweit ich das beurteilen kann, tatsächlich in StackExchange.Redis. Bitte korrigieren Sie mich, wenn ich falsch liege.

Ergebnisse debuggen

Ich habe festgestellt, dass der Deadlock seinen Ursprung in ProcessAsyncCompletionQueue in Zeile 124 von CompletionManager.cs zu haben scheint.

Ausschnitt aus diesem Code:

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

Ich habe das während des Deadlocks gefunden. activeAsyncWorkerThread ist einer unserer Threads, der auf den Abschluss eines Redis-Aufrufs wartet. (nser Thread = ein Thread Pool Thread läuft nser Code). Die obige Schleife wird also für immer fortgesetzt.

Ohne die Details zu kennen, fühlt sich das sicher falsch an. StackExchange.Redis wartet auf einen Thread, den es für den aktiven asynchronen Arbeitsthread hält, während es sich in Wirklichkeit um einen Thread handelt, der genau das Gegenteil davon ist.

Ich frage mich, ob dies auf das Thread-Hijacking-Problem zurückzuführen ist (was ich nicht vollständig verstehe).

Was ist zu tun?

Die beiden wichtigsten Fragen, die ich herausfinden möchte:

  1. Könnte das Mischen von await und Wait()/Result die Ursache für Deadlocks sein, selbst wenn sie ohne Synchronisationskontext ausgeführt werden?

  2. Stoßen wir in StackExchange.Redis auf einen Fehler/eine Einschränkung?

Eine mögliche Lösung?

Aus meinen Debug-Ergebnissen geht hervor, dass das Problem Folgendes ist:

next.TryComplete(true);

... in der Zeile 162 in CompletionManager.cs könnte unter bestimmten Umständen den aktuellen Thread (bei dem es sich um den aktiven asynchronen Arbeitsthread handelt) herumlaufen lassen aus und starten Sie die Verarbeitung von anderem Code, was möglicherweise zu einem Deadlock führt.

Ohne die Details zu kennen und nur über diese "Tatsache" nachzudenken, erscheint es logisch, den aktiven asynchronen Worker-Thread während des Aufrufs von TryComplete vorübergehend freizugeben.

Ich denke, dass so etwas funktionieren könnte:

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

Ich denke meine beste Hoffnung ist, dass Marc Gravell dies lesen und ein Feedback geben würde :-)

Kein Synchronisationskontext = Der Standard-Synchronisationskontext

Ich habe oben geschrieben, dass unser Code keinen Synchronisationskontext verwendet. Dies ist nur teilweise der Fall: Der Code wird entweder als Konsolenanwendung oder als Azure Worker-Rolle ausgeführt. In diesen Umgebungen ist SynchronizationContext.Currentnull, weshalb ich geschrieben habe, dass wir ohne Synchronisationskontext ausführen.

Nach dem Lesen von Alles dreht sich um den SynchronizationContext ​​ habe ich jedoch festgestellt, dass dies nicht wirklich der Fall ist:

Laut Konvention hat der aktuelle SynchronizationContext eines Threads, wenn er null ist, implizit einen Standard-SynchronizationContext.

Der Standard-Synchronisationskontext sollte jedoch nicht die Ursache für Deadlocks sein, wie dies bei einem UI-basierten Synchronisationskontext (WinForms, WPF) der Fall sein könnte - da dies keine Thread-Affinität impliziert.

Was ich denke, passiert

Wenn eine Nachricht abgeschlossen ist, wird ihre Beendigungsquelle daraufhin überprüft, ob sie als sync safe angesehen wird. Wenn dies der Fall ist, wird die Abschlussaktion inline ausgeführt und alles ist in Ordnung.

Ist dies nicht der Fall, besteht die Idee darin, die Abschlussaktion für einen neu zugewiesenen Threadpool-Thread auszuführen. Auch dies funktioniert einwandfrei, wenn ConnectionMultiplexer.PreserveAsyncOrderfalse ist.

Wenn jedoch ConnectionMultiplexer.PreserveAsyncOrdertrue (der Standardwert) ist, serialisieren diese Thread-Pool-Threads ihre Arbeit mit einer Beendigungswarteschlange und stellen dabei sicher, dass höchstens eine von Sie sind jederzeit der aktive asynchrone Worker-Thread.

Wenn ein Thread zum aktiven asynchronen Arbeitsthread wird, wird dies so lange fortgesetzt, bis die Abschlusswarteschlange geleert wurde.

Das Problem besteht darin, dass die Abschlussaktion nicht synchronisationssicher (von oben) ist, sie jedoch auf einem Thread ausgeführt wird, der nicht blockiert werden darf da dies verhindert, dass andere nicht synchronisierungssichere Nachrichten abgeschlossen werden.

Beachten Sie, dass andere Nachrichten, die mit einer Abschlussaktion abgeschlossen werden, die synchronisationssicher ist, weiterhin einwandfrei funktionieren, obwohl aktiver asynchroner Arbeitsthread blockiert ist.

Mein vorgeschlagenes "Update" (oben) würde auf diese Weise keinen Deadlock verursachen, es würde jedoch mit dem Begriff "Beibehalten der Reihenfolge der asynchronen Fertigstellung" in Konflikt geraten.

Vielleicht ist die Schlussfolgerung hier zu ziehen, dass es nicht sicher ist, await mit Result/Wait() zu mischen, wenn PreserveAsyncOrder ist true, egal ob wir ohne Synchronisationskontext laufen?

(Zumindest bis wir .NET 4.6 und das neue TaskCreationOptions.RunContinuationsAsynchronously verwenden können, nehme ich an)

72

Dies sind die Problemumgehungen, die ich für dieses Deadlock-Problem gefunden habe:

Problemumgehung Nr. 1

Standardmäßig stellt StackExchange.Redis sicher, dass die Befehle in der Reihenfolge ausgeführt werden, in der die Ergebnisnachrichten empfangen werden. Dies kann zu einem Deadlock führen, wie in dieser Frage beschrieben.

Deaktivieren Sie dieses Verhalten, indem Sie PreserveAsyncOrder auf false setzen.

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

Dies vermeidet Deadlocks und könnte auch Performance verbessern .

Ich ermutige jeden, der auf Deadlock-Probleme stößt, diese Problemumgehung auszuprobieren, da sie so sauber und einfach ist.

Sie verlieren die Garantie, dass asynchrone Fortsetzungen in derselben Reihenfolge aufgerufen werden, in der die zugrunde liegenden Redis-Vorgänge abgeschlossen sind. Ich verstehe jedoch nicht wirklich, warum Sie sich darauf verlassen würden.


Problemumgehung Nr. 2

Der Deadlock tritt auf, wenn der aktive asynchrone Arbeitsthread in StackExchange.Redis einen Befehl ausführt und wenn die Beendigungsaufgabe inline ausgeführt wird.

Sie können verhindern, dass eine Aufgabe inline ausgeführt wird, indem Sie ein benutzerdefiniertes TaskScheduler verwenden und sicherstellen, dass TryExecuteTaskInlinefalse zurückgibt.

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

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

Die Implementierung eines guten Taskplaners kann eine komplexe Aufgabe sein. Es gibt jedoch bereits Implementierungen in der ParallelExtensionExtras-Bibliothek ( NuGet-Paket ), von denen Sie Gebrauch machen oder sich inspirieren lassen können.

Wenn Ihr Taskplaner eigene Threads verwenden würde (nicht aus dem Thread-Pool), ist es möglicherweise eine gute Idee, Inlining zuzulassen, es sei denn, der aktuelle Thread stammt aus dem Thread-Pool. Dies funktioniert, weil der aktive asynchrone Arbeitsthread in StackExchange.Redis immer ein Threadpool-Thread ist.

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

Eine andere Idee wäre, Ihren Scheduler mit thread-local storage an alle Threads anzuhängen.

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

Stellen Sie sicher, dass dieses Feld zugewiesen ist, wenn der Thread ausgeführt und nach Abschluss gelöscht wird:

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

Dann können Sie das Inlinen von Aufgaben zulassen, solange diese in einem Thread ausgeführt werden, der dem benutzerdefinierten Scheduler "gehört":

public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
    // Allow inlining on our own threads.
    return __attachedScheduler.Value == this && this.TryExecuteTask(task);
}
21

Ich vermute viel basierend auf den obigen detaillierten Informationen und weiß nicht, welchen Quellcode Sie haben. Es hört sich so an, als ob Sie einige interne und konfigurierbare Grenzen in .Net überschreiten. Sie sollten diese nicht treffen, daher schätze ich, dass Sie keine Objekte entsorgen, da sie zwischen Threads schweben, sodass Sie keine using-Anweisung verwenden können, um die Lebensdauer ihrer Objekte sauber zu handhaben.

Hier werden die Einschränkungen für HTTP-Anforderungen erläutert. Ähnlich wie beim alten WCF-Problem, wenn Sie die Verbindung nicht freigegeben haben und dann alle WCF-Verbindungen fehlschlagen würden.

Maximale Anzahl gleichzeitiger HttpWebRequests

Dies ist eher eine Debugging-Hilfe, da ich bezweifle, dass Sie wirklich alle TCP Ports verwenden, aber gute Informationen darüber, wie viele offene Ports Sie haben und wohin.

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

0
Josh