Deadlock beim Zugriff auf StackExchange.Redis

73

Beim Aufruf von StackExchange.Redis gerate ich in eine Deadlock-Situation .

Ich weiß nicht genau, was los ist, was sehr frustrierend ist, und ich würde mich über jede Eingabe freuen, die zur Lösung oder Umgehung dieses Problems beitragen könnte.


Falls Sie dieses Problem auch haben und dies alles nicht lesen möchten; Ich schlage vor , dass Sie Einstellung versuchen würden PreserveAsyncOrderzu false.

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

Dies wird wahrscheinlich die Art von Deadlock beheben, um die es in diesen Fragen und Antworten 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 weisen eine Thread-Affinität auf (gehört einem einzelnen Thread und muss von diesem ausgeführt werden).
  • Einige Teile des Codes sind nur asynchron.
  • Wir führen die Sync-over-Async- und Async-over-Sync- Anti-Patterns durch. (Mischen awaitund 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, läuft sie eine Weile normal, dann funktionieren plötzlich (fast) alle eingehenden Anforderungen nicht mehr und sie erzeugen nie eine Antwort. Alle diese Anforderungen sind blockiert und warten darauf, dass ein Anruf bei Redis abgeschlossen wird.

Interessanterweise bleibt jeder Aufruf von Redis hängen, sobald der Deadlock auftritt, jedoch nur, wenn diese Aufrufe von einer eingehenden API-Anforderung stammen, die im Thread-Pool ausgeführt wird.

Wir rufen Redis auch von Hintergrund-Threads mit niedriger Priorität an, und diese Aufrufe funktionieren auch nach dem Auftreten des Deadlocks weiter.

Es scheint, als würde ein Deadlock nur auftreten, wenn Redis in einem Thread-Pool-Thread aufgerufen wird. Ich denke nicht mehr, dass dies auf die Tatsache zurückzuführen ist, dass diese Aufrufe in einem Thread-Pool-Thread erfolgen. Es scheint eher so, als würde jeder asynchrone Redis-Aufruf ohne Fortsetzung oder mit einer synchronen sicheren Fortsetzung auch nach dem Auftreten der Deadlock-Situation weiter funktionieren. (Siehe, was meiner Meinung nach unten passiert )

verbunden

  • StackExchange.Redis Deadlocking

    Deadlock durch Mischen awaitund Task.Result(Sync-over-Async, wie wir). Unser Code wird jedoch ohne Synchronisationskontext ausgeführt, sodass dies hier nicht zutrifft, oder?

  • Wie kann man Sync- und Async-Code 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, daher sollte dies keine Deadlocks verursachen, oder?

    Die Einstellung ConfigureAwait(false)vor einer awaithat keine Auswirkung darauf.

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

    Dies ist das Problem der Thread-Entführung. Wie ist die aktuelle Situation dazu? Könnte dies hier das Problem sein?

  • Der asynchrone Aufruf von StackExchange.Redis hängt

    Aus Marc's Antwort:

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

    Er sagt aber auch:

    SE.Redis umgeht den Synchronisationskontext intern (normal für Bibliothekscode), daher sollte es keinen Deadlock geben

    Nach meinem Verständnis sollte StackExchange.Redis daher unabhängig davon sein, ob wir das Sync-over-Async- Anti-Pattern verwenden. Es wird einfach 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, falls ich falsch liege.

Debug-Ergebnisse

Ich habe festgestellt , dass die Blockade die Quelle in zu haben scheint ProcessAsyncCompletionQueueauf Linie 124 vonCompletionManager.cs .

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; activeAsyncWorkerThreadist einer unserer Threads, der darauf wartet, dass ein Redis-Aufruf abgeschlossen wird. ( unser Thread = ein Thread-Pool-Thread, in dem unser Code ausgeführt wird ). 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, von dem es glaubt, dass er der aktive asynchrone Worker-Thread ist, während es sich tatsächlich um einen Thread handelt, der genau das Gegenteil davon ist.

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

Was ist zu tun?

Die beiden wichtigsten Fragen, die ich herauszufinden versuche:

  1. Könnte das Mischen awaitund Wait()/ Resultoder die Ursache für Deadlocks sein, selbst wenn es ohne Synchronisationskontext ausgeführt wird?

  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 darin besteht, dass:

next.TryComplete(true);

... in Zeile 162 inCompletionManager.cs könnte unter bestimmten Umständen den aktuellen Thread (der der aktive asynchrone Worker-Thread ist ) abwandern lassen und mit der Verarbeitung von anderem Code beginnen, was möglicherweise zu einem Deadlock führen kann.

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

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 liest und Feedback gibt :-)

Kein Synchronisationskontext = Der Standard-Synchronisationskontext

Ich habe oben geschrieben, dass unser Code keinen Synchronisationskontext verwendet . Dies ist nur teilweise richtig: Der Code wird entweder als Konsolenanwendung oder als Azure Worker-Rolle ausgeführt. In diesen Umgebungen SynchronizationContext.Currentist null, weshalb ich schrieb , dass wir laufen ohne Synchronisationskontext.

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

Wenn der aktuelle SynchronizationContext eines Threads null ist, hat er implizit einen Standard-SynchronizationContext.

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

Was ich denke passiert

Wenn eine Nachricht abgeschlossen ist, wird ihre Abschlussquelle daraufhin überprüft, ob sie als synchronisationssicher gilt . 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 Thread-Pool-Thread auszuführen. Auch das funktioniert gut, wenn es ConnectionMultiplexer.PreserveAsyncOrderist false.

Wenn dies ConnectionMultiplexer.PreserveAsyncOrderjedoch trueder Standardwert ist, serialisieren diese Thread-Pool-Threads ihre Arbeit mithilfe einer Abschlusswarteschlange und stellen sicher, dass höchstens einer von ihnen zu jedem Zeitpunkt der aktive asynchrone Worker-Thread ist .

Wenn ein Thread zum aktiven asynchronen Worker-Thread wird, bleibt dies so lange bestehen, bis die Abschlusswarteschlange leer ist .

Das Problem ist, dass die Abschlussaktion nicht synchronisierungssicher ist (von oben), sie jedoch in einem Thread ausgeführt wird, der nicht blockiert werden darf, da dadurch verhindert wird, dass andere nicht synchronisierungssichere Nachrichten abgeschlossen werden.

Beachten Sie, dass andere Nachrichten, die mit einer synchronisierungssicheren Abschlussaktion abgeschlossen werden, weiterhin einwandfrei funktionieren, obwohl der aktive asynchrone Worker-Thread blockiert ist.

Mein vorgeschlagener "Fix" (oben) würde auf diese Weise keinen Deadlock verursachen, würde jedoch den Gedanken , die asynchrone Abschlussreihenfolge beizubehalten, durcheinander bringen .

Also vielleicht der Abschluss hier zu machen ist , dass es nicht sicher ist , zu mischen awaitmit Result/ Wait()wenn PreserveAsyncOrderisttrue , ganz gleich , ob wir ohne Synchronisationskontext laufen?

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

Mårten Wikström
quelle
Es ist sehr schwierig, sich hier eine Meinung zu bilden, da Sie keinen der Codes anzeigen, die tatsächlich SE.Redis aufrufen, oder warten / warten - welches ist der kritische Code ... können Sie zeigen, dass Sie ihn aufrufen?
Marc Gravell
@MarcGravell: Ich kann Ihnen jeden Code zeigen, wenn auch nicht in seiner Gesamtheit. Das Problem ist jedoch, dass ich nicht weiß, welcher Code hier der interessante Teil ist. Bitte beachten Sie meine letzte Bearbeitung (am Ende). Ich denke, das Problem ist generisch und beruht auf einer nicht synchronen sicheren Abschlussaktion, die vom aktiven asynchronen Worker-Thread ausgeführt wird und beim Blockieren einen Deadlock verursacht.
Mårten Wikström
2
Obwohl keine Antwort, was für eine gut geschriebene Frage.
Nico
Der Sync-Over-Async-Deadlock wird auch in asp.net-Anwendungen verursacht, wenn der Synchronisationskontext, der die Async-Methode aufgerufen hat, derjenige ist, zu dem versucht wird, zurückzukehren, selbst wenn er von einem Hintergrundthread stammt.
eran otzap
Ich sehe dasselbe Szenario in einem bestimmten Fall, das in meiner lokalen Entwicklungsumgebung reproduzierbar ist. Ich bin mir nicht sicher, was dies auslöst, aber es ist genau das gleiche Deadlock-Symptom - qs sagt, dass Sachen gesendet werden, in sagt, dass Sachen empfangen werden, aber es hängt. Dies geschieht mit vollständig synchronisierten Aufrufen an SE Redis, überhaupt nicht asynchron. Das Festlegen von PreserveAsyncOrder behebt dies, aber das scheint irgendwie magisch. @MarcGravell irgendwelche Ideen dazu?
Chris Hynes

Antworten:

23

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

Problemumgehung Nr. 1

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

Deaktivieren Sie dieses Verhalten durch das Setzen PreserveAsyncOrderauf false.

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

Dies vermeidet Deadlocks und kann auch die Leistung 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 Worker-Thread in StackExchange.Redis einen Befehl abschließt und wenn die Abschlussaufgabe inline ausgeführt wird.

Sie können verhindern, dass eine Aufgabe inline ausgeführt wird, indem Sie eine benutzerdefinierte Aufgabe verwenden TaskSchedulerund sicherstellen, dass diese TryExecuteTaskInlinezurückgegeben wird 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...
}

Das Implementieren eines guten Aufgabenplaners kann eine komplexe Aufgabe sein. Es gibt jedoch bereits Implementierungen in der ParallelExtensionExtras-Bibliothek ( NuGet-Paket ), die Sie verwenden oder von denen Sie 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, da der aktive asynchrone Worker-Thread in StackExchange.Redis immer ein Thread-Pool-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 mithilfe des thread-lokalen Speichers an alle seine Threads anzuhängen .

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

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

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 Inlining 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);
}
Mårten Wikström
quelle
2
Hinweis: Ab Release 2.0.495 PreserveAsyncOrder ist veraltet.
Tehmas
@tehmas irgendwelche Vorschläge, wo sich die neue Flagge befindet, ConnectionMultiplexernachdem sie PreserveAsyncOrderveraltet ist? Oder wenn irgendwo anders eine Flagge ist StackExchange.Redis?
chy600
-1

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 in .Net einige interne und konfigurierbare Grenzwerte erreichen. Sie sollten diese nicht treffen, daher vermute ich, dass Sie keine Objekte entsorgen, da diese zwischen Threads schweben, sodass Sie keine using-Anweisung verwenden können, um ihre Objektlebensdauer sauber zu handhaben.

Hier werden die Einschränkungen für HTTP-Anforderungen aufgeführt. Ähnlich wie beim alten WCF-Problem, wenn Sie die Verbindung nicht entsorgt 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 Sie feststellen können, wie viele offene Ports Sie haben und wohin.

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

Josh
quelle
Vielen Dank. Dieses Problem wird jedoch nicht dadurch verursacht, dass keine TCP-Ports oder HTTP-Verbindungen mehr vorhanden sind.
Mårten Wikström